Merge branch 'master' into chat-message-notification-e2e-tests

This commit is contained in:
mahula 2025-05-06 08:41:35 +02:00 committed by GitHub
commit 9b989431f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 6576 additions and 1696 deletions

View File

@ -4,8 +4,40 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [3.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.4.0...3.5.0)
- feat(webapp): user teaser popover [`#8450`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8450)
- feat(backend): signup email localized [`#8459`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8459)
- lint json [`#8472`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8472)
- refactor(other): cypress: simplify cucumber preprocessor imports and some linting [`#8489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8489)
- fix backend node23 [`#8488`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8488)
- refactor(workflow): parallelize e2e preparation [`#8481`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8481)
- refactor(backend): types for context + `slug` [`#8486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8486)
- feat(backend): emails for notifications [`#8435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8435)
- remove some dependabot groups & no alpine version to allow update [`#8475`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8475)
- build(deps-dev): bump the babel group with 3 updates [`#8478`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8478)
- build(deps-dev): bump @types/node from 22.15.2 to 22.15.3 in /backend [`#8479`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8479)
- refactor(backend): refactor context [`#8434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8434)
- build(deps): bump amannn/action-semantic-pull-request [`#8480`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8480)
- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8332)
- remove all helpers on src/helpers [`#8469`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8469)
- move models into database folder [`#8471`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8471)
- also lint cjs files [`#8467`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8467)
- refactor(backend): refactor badges [`#8465`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8465)
- refactor(backend): move resolvers into graphql folder [`#8470`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8470)
- refactor(webapp): remove unused packages [`#8468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8468)
- refactor(backend): remove unused packages [`#8466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8466)
- fix(backend): fix backend dev and dev:debug command [`#8439`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8439)
- move distanceToMe onto Location [`#8464`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8464)
- feat(backend): distanceToMe [`#8462`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8462)
- fix(webapp): fixed padding for mobile in basic layout [`#8455`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8455)
- Fix ocelot.social link for imprint and donation [`#8461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8461)
#### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0)
> 28 April 2025
- v3.4.0 [`#8454`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8454)
- fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452)
- feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429)
- refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440)

View File

@ -223,5 +223,10 @@ module.exports = {
'jest/unbound-method': 'error',
},
},
{
extends: ['plugin:jsonc/recommended-with-jsonc'],
files: ['*.json', '*.json5', '*.jsonc'],
parser: 'jsonc-eslint-parser',
},
],
}

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "3.4.0",
"version": "3.5.0",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -12,7 +12,7 @@
"build": "tsc && tsc-alias && ./scripts/build.copy.files.sh",
"dev": "nodemon --exec ts-node --require tsconfig-paths/register src/index.ts -e js,ts,gql",
"dev:debug": "nodemon --exec node --inspect=0.0.0.0:9229 build/src/index.js -e js,ts,gql",
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs .",
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc .",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
"db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts",
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
@ -106,6 +106,7 @@
"eslint-import-resolver-typescript": "^4.3.4",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsonc": "^2.20.0",
"eslint-plugin-n": "^17.17.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.6",

View File

@ -1,257 +1,234 @@
[
{
"provider_name": "Codepen",
"provider_url": "https:\/\/codepen.io",
"endpoints": [
{
"schemes": [
"http:\/\/codepen.io\/*",
"https:\/\/codepen.io\/*"
],
"url": "http:\/\/codepen.io\/api\/oembed"
}
]
},
{
"provider_name": "DTube",
"provider_url": "https:\/\/d.tube\/",
"endpoints": [
{
"schemes": [
"https:\/\/d.tube\/v\/*"
],
"url": "https:\/\/api.d.tube\/oembed",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Post)",
"provider_url": "https:\/\/www.facebook.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/www.facebook.com\/*\/posts\/*",
"https:\/\/www.facebook.com\/photos\/*",
"https:\/\/www.facebook.com\/*\/photos\/*",
"https:\/\/www.facebook.com\/photo.php*",
"https:\/\/www.facebook.com\/photo.php",
"https:\/\/www.facebook.com\/*\/activity\/*",
"https:\/\/www.facebook.com\/permalink.php",
"https:\/\/www.facebook.com\/media\/set?set=*",
"https:\/\/www.facebook.com\/questions\/*",
"https:\/\/www.facebook.com\/notes\/*\/*\/*"
],
"url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Video)",
"provider_url": "https:\/\/www.facebook.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/www.facebook.com\/*\/videos\/*",
"https:\/\/www.facebook.com\/video.php"
],
"url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Flickr",
"provider_url": "https:\/\/www.flickr.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/*.flickr.com\/photos\/*",
"http:\/\/flic.kr\/p\/*",
"https:\/\/*.flickr.com\/photos\/*",
"https:\/\/flic.kr\/p\/*"
],
"url": "https:\/\/www.flickr.com\/services\/oembed\/",
"discovery": true
}
]
},
{
"provider_name": "GIPHY",
"provider_url": "https:\/\/giphy.com",
"endpoints": [
{
"schemes": [
"https:\/\/giphy.com\/gifs\/*",
"http:\/\/gph.is\/*",
"https:\/\/media.giphy.com\/media\/*\/giphy.gif"
],
"url": "https:\/\/giphy.com\/services\/oembed",
"discovery": true
}
]
},
{
"provider_name": "Instagram",
"provider_url": "https:\/\/instagram.com",
"endpoints": [
{
"schemes": [
"http:\/\/instagram.com\/p\/*",
"http:\/\/instagr.am\/p\/*",
"http:\/\/www.instagram.com\/p\/*",
"http:\/\/www.instagr.am\/p\/*",
"https:\/\/instagram.com\/p\/*",
"https:\/\/instagr.am\/p\/*",
"https:\/\/www.instagram.com\/p\/*",
"https:\/\/www.instagr.am\/p\/*"
],
"url": "https:\/\/api.instagram.com\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "Meetup",
"provider_url": "http:\/\/www.meetup.com",
"endpoints": [
{
"schemes": [
"http:\/\/meetup.com\/*",
"https:\/\/www.meetup.com\/*",
"https:\/\/meetup.com\/*",
"http:\/\/meetu.ps\/*"
],
"url": "https:\/\/api.meetup.com\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "MixCloud",
"provider_url": "https:\/\/mixcloud.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/www.mixcloud.com\/*\/*\/",
"https:\/\/www.mixcloud.com\/*\/*\/"
],
"url": "https:\/\/www.mixcloud.com\/oembed\/"
}
]
},
{
"provider_name": "Reddit",
"provider_url": "https:\/\/reddit.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/reddit.com\/r\/*\/comments\/*\/*",
"https:\/\/www.reddit.com\/r\/*\/comments\/*\/*"
],
"url": "https:\/\/www.reddit.com\/oembed"
}
]
},
{
"provider_name": "SlideShare",
"provider_url": "http:\/\/www.slideshare.net\/",
"endpoints": [
{
"schemes": [
"http:\/\/www.slideshare.net\/*\/*",
"http:\/\/fr.slideshare.net\/*\/*",
"http:\/\/de.slideshare.net\/*\/*",
"http:\/\/es.slideshare.net\/*\/*",
"http:\/\/pt.slideshare.net\/*\/*"
],
"url": "http:\/\/www.slideshare.net\/api\/oembed\/2",
"discovery": true
}
]
},
{
"provider_name": "SoundCloud",
"provider_url": "http:\/\/soundcloud.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/soundcloud.com\/*",
"https:\/\/soundcloud.com\/*"
],
"url": "https:\/\/soundcloud.com\/oembed"
}
]
},
{
"provider_name": "Twitch",
"provider_url": "https:\/\/www.twitch.tv",
"endpoints": [
{
"schemes": [
"http:\/\/clips.twitch.tv\/*",
"https:\/\/clips.twitch.tv\/*",
"http:\/\/www.twitch.tv\/*",
"https:\/\/www.twitch.tv\/*",
"http:\/\/twitch.tv\/*",
"https:\/\/twitch.tv\/*"
],
"url": "https:\/\/api.twitch.tv\/v4\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "Twitter",
"provider_url": "http:\/\/www.twitter.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/twitter.com\/*\/status\/*",
"https:\/\/*.twitter.com\/*\/status\/*"
],
"url": "https:\/\/publish.twitter.com\/oembed"
}
]
},
{
"provider_name": "Vimeo",
"provider_url": "https:\/\/vimeo.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/vimeo.com\/*",
"https:\/\/vimeo.com\/album\/*\/video\/*",
"https:\/\/vimeo.com\/channels\/*\/*",
"https:\/\/vimeo.com\/groups\/*\/videos\/*",
"https:\/\/vimeo.com\/ondemand\/*\/*",
"https:\/\/player.vimeo.com\/video\/*"
],
"url": "https:\/\/vimeo.com\/api\/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "YouTube",
"provider_url": "https:\/\/www.youtube.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/*.youtube.com\/watch*",
"https:\/\/*.youtube.com\/v\/*",
"https:\/\/youtu.be\/*"
],
"url": "https:\/\/www.youtube.com\/oembed",
"discovery": true
}
]
}
]
{
"provider_name": "Codepen",
"provider_url": "https://codepen.io",
"endpoints": [
{
"schemes": ["http://codepen.io/*", "https://codepen.io/*"],
"url": "http://codepen.io/api/oembed"
}
]
},
{
"provider_name": "DTube",
"provider_url": "https://d.tube/",
"endpoints": [
{
"schemes": ["https://d.tube/v/*"],
"url": "https://api.d.tube/oembed",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Post)",
"provider_url": "https://www.facebook.com/",
"endpoints": [
{
"schemes": [
"https://www.facebook.com/*/posts/*",
"https://www.facebook.com/photos/*",
"https://www.facebook.com/*/photos/*",
"https://www.facebook.com/photo.php*",
"https://www.facebook.com/photo.php",
"https://www.facebook.com/*/activity/*",
"https://www.facebook.com/permalink.php",
"https://www.facebook.com/media/set?set=*",
"https://www.facebook.com/questions/*",
"https://www.facebook.com/notes/*/*/*"
],
"url": "https://www.facebook.com/plugins/post/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Video)",
"provider_url": "https://www.facebook.com/",
"endpoints": [
{
"schemes": ["https://www.facebook.com/*/videos/*", "https://www.facebook.com/video.php"],
"url": "https://www.facebook.com/plugins/video/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Flickr",
"provider_url": "https://www.flickr.com/",
"endpoints": [
{
"schemes": [
"http://*.flickr.com/photos/*",
"http://flic.kr/p/*",
"https://*.flickr.com/photos/*",
"https://flic.kr/p/*"
],
"url": "https://www.flickr.com/services/oembed/",
"discovery": true
}
]
},
{
"provider_name": "GIPHY",
"provider_url": "https://giphy.com",
"endpoints": [
{
"schemes": [
"https://giphy.com/gifs/*",
"http://gph.is/*",
"https://media.giphy.com/media/*/giphy.gif"
],
"url": "https://giphy.com/services/oembed",
"discovery": true
}
]
},
{
"provider_name": "Instagram",
"provider_url": "https://instagram.com",
"endpoints": [
{
"schemes": [
"http://instagram.com/p/*",
"http://instagr.am/p/*",
"http://www.instagram.com/p/*",
"http://www.instagr.am/p/*",
"https://instagram.com/p/*",
"https://instagr.am/p/*",
"https://www.instagram.com/p/*",
"https://www.instagr.am/p/*"
],
"url": "https://api.instagram.com/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "Meetup",
"provider_url": "http://www.meetup.com",
"endpoints": [
{
"schemes": [
"http://meetup.com/*",
"https://www.meetup.com/*",
"https://meetup.com/*",
"http://meetu.ps/*"
],
"url": "https://api.meetup.com/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "MixCloud",
"provider_url": "https://mixcloud.com/",
"endpoints": [
{
"schemes": ["http://www.mixcloud.com/*/*/", "https://www.mixcloud.com/*/*/"],
"url": "https://www.mixcloud.com/oembed/"
}
]
},
{
"provider_name": "Reddit",
"provider_url": "https://reddit.com/",
"endpoints": [
{
"schemes": [
"https://reddit.com/r/*/comments/*/*",
"https://www.reddit.com/r/*/comments/*/*"
],
"url": "https://www.reddit.com/oembed"
}
]
},
{
"provider_name": "SlideShare",
"provider_url": "http://www.slideshare.net/",
"endpoints": [
{
"schemes": [
"http://www.slideshare.net/*/*",
"http://fr.slideshare.net/*/*",
"http://de.slideshare.net/*/*",
"http://es.slideshare.net/*/*",
"http://pt.slideshare.net/*/*"
],
"url": "http://www.slideshare.net/api/oembed/2",
"discovery": true
}
]
},
{
"provider_name": "SoundCloud",
"provider_url": "http://soundcloud.com/",
"endpoints": [
{
"schemes": ["http://soundcloud.com/*", "https://soundcloud.com/*"],
"url": "https://soundcloud.com/oembed"
}
]
},
{
"provider_name": "Twitch",
"provider_url": "https://www.twitch.tv",
"endpoints": [
{
"schemes": [
"http://clips.twitch.tv/*",
"https://clips.twitch.tv/*",
"http://www.twitch.tv/*",
"https://www.twitch.tv/*",
"http://twitch.tv/*",
"https://twitch.tv/*"
],
"url": "https://api.twitch.tv/v4/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "Twitter",
"provider_url": "http://www.twitter.com/",
"endpoints": [
{
"schemes": ["https://twitter.com/*/status/*", "https://*.twitter.com/*/status/*"],
"url": "https://publish.twitter.com/oembed"
}
]
},
{
"provider_name": "Vimeo",
"provider_url": "https://vimeo.com/",
"endpoints": [
{
"schemes": [
"https://vimeo.com/*",
"https://vimeo.com/album/*/video/*",
"https://vimeo.com/channels/*/*",
"https://vimeo.com/groups/*/videos/*",
"https://vimeo.com/ondemand/*/*",
"https://player.vimeo.com/video/*"
],
"url": "https://vimeo.com/api/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "YouTube",
"provider_url": "https://www.youtube.com/",
"endpoints": [
{
"schemes": [
"https://*.youtube.com/watch*",
"https://*.youtube.com/v/*",
"https://youtu.be/*"
],
"url": "https://www.youtube.com/oembed",
"discovery": true
}
]
}
]

View File

@ -62,6 +62,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -185,6 +189,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;

View File

@ -0,0 +1,261 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendEmailVerification English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello User,</h2>
<div class="wrapper">
<div class="content"></div>
<p>So, you want to change your e-mail? No problem! Just click the button below to verify your new address:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&amp;nonce=123456">Verify e-mail address</a>
<p>If you don't want to change your e-mail address feel free to ignore this message. </p>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "New E-Mail Address ocelot.social",
"text": "HELLO USER,
So, you want to change your e-mail? No problem! Just click the button below to
verify your new address:
Verify e-mail address
[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456]
If you don't want to change your e-mail address feel free to ignore this
message.
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendEmailVerification German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo User,</h2>
<div class="wrapper">
<div class="content"></div>
<p>Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&amp;nonce=123456">E-Mail Adresse bestätigen</a>
<p>Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Neue E-Mail Addresse ocelot.social",
"text": "HALLO USER,
Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
kannst Du Deine neue E-Mail Adresse bestätigen:
E-Mail Adresse bestätigen
[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456]
Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese
Nachricht einfach ignorieren.
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: 123456
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -62,6 +62,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -184,6 +188,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -308,6 +316,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -432,6 +444,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -556,6 +572,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -679,6 +699,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -801,6 +825,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -920,6 +948,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1043,6 +1075,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1166,6 +1202,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1288,6 +1328,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1412,6 +1456,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1536,6 +1584,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1660,6 +1712,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1783,6 +1839,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -1905,6 +1965,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -2024,6 +2088,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
@ -2147,6 +2215,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;

View File

@ -0,0 +1,559 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendRegistrationMail with invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Thank you for joining our cause it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;inviteCode=welcome&amp;method=invite-code">Confirm your e-mail address</a>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<p>However, this only works if you have registered through our website.</p>
<p>If you didn't sign up for <a>ocelot.social</a> we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.
</p>
<p>PS: If you ignore this e-mail we will not create an account for you. ;)</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Welcome to ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
Thank you for joining our cause it's awesome to have you on board. There's
just one tiny step missing before we can start shaping the world together …
Please confirm your e-mail address by clicking the button below:
Confirm your e-mail address
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code]
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
However, this only works if you have registered through our website.
If you didn't sign up for ocelot.social we recommend you to check it out! It's a
social network from people for people who want to connect and change the world
together.
PS: If you ignore this e-mail we will not create an account for you. ;)
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail with invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;inviteCode=welcome&amp;method=invite-code">Bestätige Deine E-Mail Adresse</a>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
</p>
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Willkommen bei ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
… Bitte bestätige Deine E-Mail Adresse:
Bestätige Deine E-Mail Adresse
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code]
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: 123456
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
hast.
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
ignorieren. ;)
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail without invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Thank you for joining our cause it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;method=invite-mail">Confirm your e-mail address</a>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<p>However, this only works if you have registered through our website.</p>
<p>If you didn't sign up for <a>ocelot.social</a> we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.
</p>
<p>PS: If you ignore this e-mail we will not create an account for you. ;)</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Welcome to ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
Thank you for joining our cause it's awesome to have you on board. There's
just one tiny step missing before we can start shaping the world together …
Please confirm your e-mail address by clicking the button below:
Confirm your e-mail address
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail]
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
However, this only works if you have registered through our website.
If you didn't sign up for ocelot.social we recommend you to check it out! It's a
social network from people for people who want to connect and change the world
together.
PS: If you ignore this e-mail we will not create an account for you. ;)
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail without invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;method=invite-mail">Bestätige Deine E-Mail Adresse</a>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
</p>
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Willkommen bei ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
… Bitte bestätige Deine E-Mail Adresse:
Bestätige Deine E-Mail Adresse
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail]
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: 123456
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
hast.
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
ignorieren. ;)
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,260 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendResetPasswordMail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello Jenny Rostock,</h2>
<div class="wrapper">
<div class="content"></div>
<p>So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&amp;nonce=123456">Confirm your e-mail address</a>
<p>If you didn't request a new password feel free to ignore this e-mail.</p>
<p>If the above button doesn't work you can also copy the following code into your browser window: <span>123456</span></p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Reset Password ocelot.social",
"text": "HELLO JENNY ROSTOCK,
So, you forgot your password? No problem! Just click the button below to reset
it within the next 24 hours:
Confirm your e-mail address
[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456]
If you didn't request a new password feel free to ignore this e-mail.
If the above button doesn't work you can also copy the following code into your
browser window: 123456
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendResetPasswordMail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo Jenny Rostock,</h2>
<div class="wrapper">
<div class="content"></div>
<p>Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&amp;nonce=123456">Bestätige Deine E-Mail Adresse</a>
<p>Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.</p>
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Neues Passwort ocelot.social",
"text": "HALLO JENNY ROSTOCK,
Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:
Bestätige Deine E-Mail Adresse
[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456]
Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach
ignorieren.
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
Dein Browserfenster kopieren: 123456
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendWrongEmail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?</p><a class="button" href="http://webapp:3000/password-reset/request">Try a different e-mail</a>
<p>If you don't have an account at <a>ocelot.social</a> yet or if you didn't want to reset your password, please ignore this e-mail.
</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Wrong E-mail? ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
You requested a password reset but unfortunately we couldn't find an account
associated with your e-mail address. Did you maybe use another one when you
signed up?
Try a different e-mail [http://webapp:3000/password-reset/request]
If you don't have an account at ocelot.social yet or if you didn't want to reset
your password, please ignore this e-mail.
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendWrongEmail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?</p><a class="button" href="http://webapp:3000/password-reset/request">Versuch' es mit einer anderen E-Mail</a>
<p>Wenn du noch keinen Account bei <a>ocelot.social</a> hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!
</p>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Falsche Mailaddresse? ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen
Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer
anderen Adresse bei uns angemeldet bist?
Versuch' es mit einer anderen E-Mail [http://webapp:3000/password-reset/request]
Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht
ändern willst, kannst du diese E-Mail einfach ignorieren!
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -7,12 +7,32 @@
"followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer",
"mentionedInComment": "Erwähnung in Kommentar",
"mentionedInPost": "Erwähnung in Beitrag",
"newEmail": "Neue E-Mail Addresse",
"removedUserFromGroup": "Aus Gruppe entfernt",
"postInGroup": "Neuer Beitrag in Gruppe",
"resetPassword": "Neues Passwort",
"userJoinedGroup": "Nutzer tritt Gruppe bei",
"userLeftGroup": "Nutzer verlässt Gruppe"
"userLeftGroup": "Nutzer verlässt Gruppe",
"wrongEmail": "Falsche Mailaddresse?"
},
"registration": {
"introduction": "Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:",
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
"codeHintException": "Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.",
"notYouStart": "Falls Du Dich nicht selbst bei ",
"notYouEnd": " angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.",
"ps": "PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)"
},
"emailVerification": {
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
"introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:",
"doNotChange": "Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. "
},
"buttons": {
"confirmEmail": "Bestätige Deine E-Mail Adresse",
"resetPassword": "Passwort zurücksetzen",
"tryAgain": "Versuch' es mit einer anderen E-Mail",
"verifyEmail": "E-Mail Adresse bestätigen",
"viewChat": "Chat anzeigen",
"viewComment": "Kommentar ansehen",
"viewGroup": "Gruppe ansehen",
@ -23,7 +43,19 @@
"seeYou": "Bis bald bei ",
"yourTeam": " Dein {team} Team",
"settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ",
"settingsName": "Benachrichtigungseinstellungen"
"settingsName": "Benachrichtigungseinstellungen",
"welcome": "Willkommen bei"
},
"resetPassword": {
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: ",
"ignore": "Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.",
"introduction": "Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:"
},
"wrongEmail": {
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
"ignoreEnd": " hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!",
"ignoreStart": "Wenn du noch keinen Account bei ",
"introduction": "Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit deiner E-Mailadresse gefunden. Kann es sein, dass du mit einer anderen Adresse bei uns angemeldet bist?"
},
"changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:",
"chatMessageStart": "du hast eine neue Chat-Nachricht von ",

View File

@ -7,12 +7,32 @@
"followedUserPosted": "New post by followd user",
"mentionedInComment": "Mentioned in comment",
"mentionedInPost": "Mentioned in post",
"newEmail": "New E-Mail Address",
"removedUserFromGroup": "Removed from group",
"postInGroup": "New post in group",
"resetPassword": "Reset Password",
"userJoinedGroup": "User joined group",
"userLeftGroup": "User left group"
"userLeftGroup": "User left group",
"wrongEmail": "Wrong E-mail?"
},
"registration": {
"introduction": "Thank you for joining our cause it's awesome to have you on board. There's just one tiny step missing before we can start shaping the world together … Please confirm your e-mail address by clicking the button below:",
"codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ",
"codeHintException": "However, this only works if you have registered through our website.",
"notYouStart": "If you didn't sign up for ",
"notYouEnd": " we recommend you to check it out! It's a social network from people for people who want to connect and change the world together.",
"ps": "PS: If you ignore this e-mail we will not create an account for you. ;)"
},
"emailVerification": {
"codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ",
"introduction": "So, you want to change your e-mail? No problem! Just click the button below to verify your new address:",
"doNotChange": "If you don't want to change your e-mail address feel free to ignore this message. "
},
"buttons": {
"confirmEmail": "Confirm your e-mail address",
"resetPassword": "Reset password",
"tryAgain": "Try a different e-mail",
"verifyEmail": "Verify e-mail address",
"viewChat": "Show Chat",
"viewComment": "View comment",
"viewGroup": "View group",
@ -23,7 +43,18 @@
"seeYou": "See you soon on ",
"yourTeam": " The {team} Team",
"settingsHint": "PS: If you don't want to receive e-mails anymore, change your ",
"settingsName": "notification settings"
"settingsName": "notification settings",
"welcome": "Welcome to"
},
"resetPassword": {
"codeHint": "If the above button doesn't work you can also copy the following code into your browser window: ",
"ignore": "If you didn't request a new password feel free to ignore this e-mail.",
"introduction": "So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:"
},
"wrongEmail": {
"ignoreEnd": " yet or if you didn't want to reset your password, please ignore this e-mail.",
"ignoreStart": "If you don't have an account at ",
"introduction": "You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address. Did you maybe use another one when you signed up?"
},
"changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:",
"chatMessageStart": "you have received a new chat message from ",

View File

@ -28,6 +28,7 @@ const defaultParams = {
ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
supportUrl: CONFIG.SUPPORT_URL,
settingsUrl,
renderSettingsUrl: true,
}
export const transport = createTransport({
@ -202,3 +203,137 @@ export const sendChatMessageMail = async (
throw new Error(error)
}
}
interface VerifyMailInput {
email: string
nonce: string
locale: string
}
interface RegistrationMailInput extends VerifyMailInput {
inviteCode?: string
}
export const sendRegistrationMail = async (
data: RegistrationMailInput,
): Promise<OriginalMessage> => {
const { nonce, locale, inviteCode } = data
const to = data.email
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
actionUrl.searchParams.set('nonce', nonce)
if (inviteCode) {
actionUrl.searchParams.set('inviteCode', inviteCode)
actionUrl.searchParams.set('method', 'invite-code')
} else {
actionUrl.searchParams.set('method', 'invite-mail')
}
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'registration'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
interface EmailVerificationInput extends VerifyMailInput {
name: string
}
export const sendEmailVerification = async (
data: EmailVerificationInput,
): Promise<OriginalMessage> => {
const { nonce, locale, name } = data
const to = data.email
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
actionUrl.searchParams.set('nonce', nonce)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'emailVerification'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
name,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export const sendResetPasswordMail = async (
data: EmailVerificationInput,
): Promise<OriginalMessage> => {
const { nonce, locale, name } = data
const to = data.email
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
actionUrl.searchParams.set('nonce', nonce)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'resetPassword'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
name,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export const sendWrongEmail = async (data: {
locale: string
email: string
}): Promise<OriginalMessage> => {
const { locale } = data
const to = data.email
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'wrongEmail'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}

View File

@ -0,0 +1,35 @@
import { sendEmailVerification } from './sendEmail'
describe('sendEmailVerification', () => {
const data: {
email: string
nonce: string
locale: string
name: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
name: 'User',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendEmailVerification(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendEmailVerification(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,63 @@
import { sendRegistrationMail } from './sendEmail'
describe('sendRegistrationMail', () => {
const data: {
email: string
nonce: string
locale: string
inviteCode?: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
inviteCode: 'welcome',
}
describe('with invite code', () => {
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
data.inviteCode = 'welcome'
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
data.inviteCode = 'welcome'
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
})
describe('without invite code', () => {
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
delete data.inviteCode
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
delete data.inviteCode
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
})
})

View File

@ -0,0 +1,35 @@
import { sendResetPasswordMail } from './sendEmail'
describe('sendResetPasswordMail', () => {
const data: {
email: string
nonce: string
locale: string
name: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
name: 'Jenny Rostock',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,31 @@
import { sendWrongEmail } from './sendEmail'
describe('sendWrongEmail', () => {
const data: {
email: string
locale: string
} = {
email: 'user@example.org',
locale: 'en',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendWrongEmail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendWrongEmail(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,10 @@
extend ../layout.pug
block content
.content
p= t('emailVerification.introduction')
a.button(href=actionUrl)= t('buttons.verifyEmail')
p= t('emailVerification.doNotChange')
p= t('emailVerification.codeHint')
span= nonce

View File

@ -0,0 +1 @@
= `${t('subjects.newEmail')} ${APPLICATION_NAME}`

View File

@ -3,12 +3,15 @@
- var organizationUrl = ORGANIZATION_URL
- var team = APPLICATION_NAME
- var settingsUrl = settingsUrl
- var renderSettingsUrl = renderSettingsUrl
p= t('general.seeYou')
a.organization(href=organizationUrl)= team
| !
p= t('general.yourTeam', { team })
br
p= t('general.settingsHint')
a.settings(href=settingsUrl)= t('general.settingsName')
| !
if renderSettingsUrl
br
p= t('general.settingsHint')
a.settings(href=settingsUrl)= t('general.settingsName')
| !

View File

@ -50,6 +50,10 @@ a.button {
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;

View File

@ -0,0 +1 @@
h2= `${t('general.welcome')} ${APPLICATION_NAME}!`

View File

@ -13,11 +13,15 @@ html(lang=locale)
.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}
style
include includes/webflow.css
- var name = name
body
div.container
include includes/header.pug
include includes/salutation.pug
if name
include includes/salutation.pug
else
include includes/welcome.pug
.wrapper
block content

View File

@ -0,0 +1,15 @@
extend ../layout.pug
block content
.content
p= t('registration.introduction')
a.button(href=actionUrl)= t('buttons.confirmEmail')
p= t('registration.codeHint')
span= nonce
p= t('registration.codeHintException')
p= t('registration.notYouStart')
a(href=ORGANIZATION_LINK)= APPLICATION_NAME
= t('registration.notYouEnd')
p= t('registration.ps')

View File

@ -0,0 +1 @@
= `${t('general.welcome')} ${APPLICATION_NAME}`

View File

@ -0,0 +1,9 @@
extend ../layout.pug
block content
.content
p= t('resetPassword.introduction')
a.button(href=actionUrl)= t('buttons.confirmEmail')
p= t('resetPassword.ignore')
p= t('resetPassword.codeHint')
span= nonce

View File

@ -0,0 +1 @@
= `${t('subjects.resetPassword')} ${APPLICATION_NAME}`

View File

@ -0,0 +1,10 @@
extend ../layout.pug
block content
.content
p= t('wrongEmail.introduction')
a.button(href=actionUrl)= t('buttons.tryAgain')
p= t('wrongEmail.ignoreStart')
a(href=ORGANIZATION_LINK)= APPLICATION_NAME
= t('wrongEmail.ignoreEnd')

View File

@ -0,0 +1 @@
= `${t('subjects.wrongEmail')} ${APPLICATION_NAME}`

View File

@ -69,6 +69,7 @@ export default {
)
return result.records.map((record) => ({
name: record.get('user').properties.name,
locale: record.get('user').properties.locale,
...record.get('email').properties,
}))
})

View File

@ -71,14 +71,14 @@ describe('passwordReset', () => {
describe('requestPasswordReset', () => {
const mutation = gql`
mutation ($email: String!) {
requestPasswordReset(email: $email)
mutation ($email: String!, $locale: String!) {
requestPasswordReset(email: $email, locale: $locale)
}
`
describe('with invalid email', () => {
beforeEach(() => {
variables = { ...variables, email: 'non-existent@example.org' }
variables = { ...variables, email: 'non-existent@example.org', locale: 'de' }
})
it('resolves anyways', async () => {
@ -96,7 +96,7 @@ describe('passwordReset', () => {
describe('with a valid email', () => {
beforeEach(() => {
variables = { ...variables, email: 'user@example.org' }
variables = { ...variables, email: 'user@example.org', locale: 'de' }
})
it('resolves', async () => {

View File

@ -50,14 +50,14 @@ afterEach(async () => {
describe('Signup', () => {
const mutation = gql`
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
mutation ($email: String!, $locale: String!, $inviteCode: String) {
Signup(email: $email, locale: $locale, inviteCode: $inviteCode) {
email
}
}
`
beforeEach(() => {
variables = { ...variables, email: 'someuser@example.org' }
variables = { ...variables, email: 'someuser@example.org', locale: 'de' }
})
describe('unauthenticated', () => {

View File

@ -9,7 +9,11 @@ type Query {
}
type Mutation {
Signup(email: String!, inviteCode: String = null): EmailAddress
Signup(
email: String!
locale: String!
inviteCode: String = null
): EmailAddress
SignupVerification(
nonce: String!
email: String!

View File

@ -245,7 +245,7 @@ type Mutation {
updateOnlineStatus(status: OnlineStatus!): Boolean!
requestPasswordReset(email: String!): Boolean!
requestPasswordReset(email: String!, locale: String!): Boolean!
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): String!

View File

@ -2,43 +2,41 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { sendMail } from '@middleware/helpers/email/sendMail'
import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
emailVerificationTemplate,
} from '@middleware/helpers/email/templateBuilder'
sendRegistrationMail,
sendEmailVerification,
sendResetPasswordMail,
} from '@src/emails/sendEmail'
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { inviteCode } = args
const { inviteCode, locale } = args
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
if (nonce) {
// emails that already exist do not have a nonce
if (inviteCode) {
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
} else {
await sendMail(signupTemplate({ email, variables: { nonce } }))
}
await sendRegistrationMail({ email, nonce, locale, inviteCode })
}
delete response.nonce
return response
}
const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => {
const { email } = args
const { email, locale } = args
const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo)
const template = userFound ? resetPasswordTemplate : wrongAccountTemplate
await sendMail(template({ email, variables: { nonce, name } }))
if (userFound) {
await sendResetPasswordMail({ email, nonce, name, locale })
} else {
// this is an antifeature allowing unauthenticated users to spam any email with wrong-email notifications
// await sendWrongEmail({ email, locale })
}
return true
}
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce, name } = response
const { email, nonce, name, locale } = response
if (nonce) {
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
await sendEmailVerification({ email, nonce, name, locale })
}
delete response.nonce
return response

View File

@ -177,8 +177,8 @@ describe('authorization', () => {
describe('access Signup', () => {
const signupMutation = gql`
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
mutation ($email: String!, $locale: String!, $inviteCode: String) {
Signup(email: $email, locale: $locale, inviteCode: $inviteCode) {
email
}
}
@ -189,6 +189,7 @@ describe('authorization', () => {
variables = {
email: 'some@email.org',
inviteCode: 'ABCDEF',
locale: 'de',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
@ -231,6 +232,7 @@ describe('authorization', () => {
variables = {
email: 'some@email.org',
inviteCode: 'ABCDEF',
locale: 'de',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = true
@ -269,6 +271,7 @@ describe('authorization', () => {
variables = {
email: 'some@email.org',
inviteCode: 'ABCDEF',
locale: 'de',
}
authenticatedUser = null
})
@ -288,6 +291,7 @@ describe('authorization', () => {
variables = {
email: 'some@email.org',
inviteCode: 'no valid invite code',
locale: 'de',
}
authenticatedUser = null
})

View File

@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
@ -25,11 +25,12 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
/* Specify a set of entries that re-map imports to additional lookup locations. */
"paths": {
"@config/*": ["./src/config/*"],
"@constants/*": ["./src/constants/*"],
"@context/*": ["./src/context/*"],
@ -66,7 +67,7 @@
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build", /* Specify an output folder for all emitted files. */
"outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
@ -88,19 +89,19 @@
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
"useUnknownInCatchVariables": false, /* Default catch clause variables as 'unknown' instead of 'any'. */
"useUnknownInCatchVariables": false /* Default catch clause variables as 'unknown' instead of 'any'. */,
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
@ -115,6 +116,6 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@ -408,6 +408,13 @@
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/eslint-utils@^4.5.1":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.11.0":
version "4.12.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
@ -1841,6 +1848,11 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.14.0, acorn@^8.5.0:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.4.1:
version "8.9.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
@ -4034,6 +4046,13 @@ eslint-compat-utils@^0.5.1:
dependencies:
semver "^7.5.4"
eslint-compat-utils@^0.6.4:
version "0.6.5"
resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz#6b06350a1c947c4514cfa64a170a6bfdbadc7ec2"
integrity sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==
dependencies:
semver "^7.5.4"
eslint-config-prettier@^10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276"
@ -4065,6 +4084,13 @@ eslint-import-resolver-typescript@^4.3.4:
tinyglobby "^0.2.13"
unrs-resolver "^1.6.3"
eslint-json-compat-utils@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz#32931d42c723da383712f25177a2c57b9ef5f079"
integrity sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==
dependencies:
esquery "^1.6.0"
eslint-module-utils@^2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b"
@ -4113,6 +4139,20 @@ eslint-plugin-jest@^28.11.0:
dependencies:
"@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
eslint-plugin-jsonc@^2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.20.0.tgz#7f3ae51abd38176487ba7324dee77578a92e15e0"
integrity sha512-FRgCn9Hzk5eKboCbVMrr9QrhM0eO4G+WKH8IFXoaeqhM/2kuWzbStJn4kkr0VWL8J5H8RYZF+Aoam1vlBaZVkw==
dependencies:
"@eslint-community/eslint-utils" "^4.5.1"
eslint-compat-utils "^0.6.4"
eslint-json-compat-utils "^0.2.1"
espree "^9.6.1 || ^10.3.0"
graphemer "^1.4.0"
jsonc-eslint-parser "^2.4.0"
natural-compare "^1.4.0"
synckit "^0.6.2 || ^0.7.3 || ^0.10.3"
eslint-plugin-n@^17.17.0:
version "17.17.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.17.0.tgz#6644433d395c2ecae0b2fe58018807e85d8e0724"
@ -4170,11 +4210,16 @@ eslint-scope@^7.2.2:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
eslint@^8.57.1:
version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
@ -4229,7 +4274,7 @@ esniff@^2.0.1:
event-emitter "^0.3.5"
type "^2.7.2"
espree@^9.6.0, espree@^9.6.1:
espree@^9.0.0, espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
@ -4238,6 +4283,15 @@ espree@^9.6.0, espree@^9.6.1:
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
"espree@^9.6.1 || ^10.3.0":
version "10.3.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==
dependencies:
acorn "^8.14.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^4.2.0"
esprima@^1.2.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.5.tgz#0993502feaf668138325756f30f9a51feeec11e9"
@ -4255,6 +4309,13 @@ esquery@^1.4.2:
dependencies:
estraverse "^5.1.0"
esquery@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
@ -6529,6 +6590,16 @@ json5@^2.2.2, json5@^2.2.3, json5@^2.x:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonc-eslint-parser@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz#74ded53f9d716e8d0671bd167bf5391f452d5461"
integrity sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==
dependencies:
acorn "^8.5.0"
eslint-visitor-keys "^3.0.0"
espree "^9.0.0"
semver "^7.3.5"
jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@ -9121,7 +9192,14 @@ string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9225,6 +9303,14 @@ synckit@^0.11.0:
"@pkgr/core" "^0.2.0"
tslib "^2.8.1"
"synckit@^0.6.2 || ^0.7.3 || ^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.10.3.tgz#940aea2c7b6d141a4f74dbdebc81e0958c331a4b"
integrity sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==
dependencies:
"@pkgr/core" "^0.2.0"
tslib "^2.8.1"
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -10087,7 +10173,16 @@ with@^7.0.0:
assert-never "^1.2.1"
babel-walk "3.0.0-canary-5"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@7.0.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

View File

@ -8,6 +8,6 @@ defineStep('I should see my comment', () => {
.get('.profile-avatar img')
.should('have.attr', 'src')
.and('contain', 'https://') // some url
.get('.user-teaser > .info > .text')
.get('.user-teaser .info > .text')
.should('contain', 'today at')
})

View File

@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "3.4.0"
appVersion: "3.5.0"

View File

@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "3.4.0"
appVersion: "3.5.0"

View File

@ -1,12 +1,12 @@
{
"name": "ocelot-social-frontend",
"version": "3.4.0",
"version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ocelot-social-frontend",
"version": "3.4.0",
"version": "3.5.0",
"license": "Apache-2.0",
"dependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-frontend",
"version": "3.4.0",
"version": "3.5.0",
"description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends",
"main": "build/index.js",
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social",
"version": "3.4.0",
"version": "3.5.0",
"description": "Free and open source software program code available to run social networks.",
"author": "ocelot.social Community",
"license": "MIT",

View File

@ -150,7 +150,7 @@ $font-size-xx-large: 2rem;
$font-size-x-large: 1.5rem;
$font-size-large: 1.25rem;
$font-size-base: 1rem;
$font-size-body: 15px;
$font-size-body: 0.938rem;
$font-size-small: 0.8rem;
$font-size-x-small: 0.7rem;
$font-size-xx-small: 0.6rem;
@ -359,37 +359,37 @@ $media-query-medium: (min-width: 768px);
$media-query-large: (min-width: 1024px);
$media-query-x-large: (min-width: 1200px);
/**
/**
* @tokens Background Images
*/
/**
/**
* @tokens Header Color
*/
$color-header-background: $color-neutral-100;
/**
/**
* @tokens Footer Color
*/
$color-footer-background: $color-neutral-100;
$color-footer-link: $color-primary;
/**
/**
* @tokens Locale Menu Color
*/
$color-locale-menu: $text-color-soft;
/**
/**
* @tokens Donation Bar Color
*/
$color-donation-bar: $color-primary;
$color-donation-bar-light: $color-primary-light;
/**
/**
* @tokens Toast Color
*/

View File

@ -52,11 +52,6 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
background: #fff;
}
body.dropdown-open {
height: 100vh;
overflow: hidden;
}
blockquote {
display: block;
padding: 15px 20px 15px 45px;
@ -140,6 +135,10 @@ hr {
opacity: 1;
transition-delay: 0;
transition: opacity 80ms ease-out;
@media(hover: none) {
pointer-events: all;
}
}
}
@ -155,7 +154,6 @@ hr {
[class$='menu-popover'] {
min-width: 130px;
a,
button {
display: flex;
align-content: center;
@ -179,4 +177,4 @@ hr {
.dropdown-arrow {
font-size: $font-size-xx-small;
}
}

View File

@ -43,32 +43,12 @@ export default {
},
watch: {
isPopoverOpen: {
immediate: true,
handler(isOpen) {
try {
if (isOpen) {
this.$nextTick(() => {
setTimeout(() => {
const paddingRightStyle = `${
window.innerWidth - document.documentElement.clientWidth
}px`
const navigationElement = document.querySelector('.main-navigation')
document.body.style.paddingRight = paddingRightStyle
document.body.classList.add('dropdown-open')
if (navigationElement) {
navigationElement.style.paddingRight = paddingRightStyle
}
}, 20)
})
} else {
const navigationElement = document.querySelector('.main-navigation')
document.body.style.paddingRight = null
document.body.classList.remove('dropdown-open')
if (navigationElement) {
navigationElement.style.paddingRight = null
}
}
} catch (err) {}
if (isOpen) {
document.body.classList.add('dropdown-open')
} else {
document.body.classList.remove('dropdown-open')
}
},
},
},

View File

@ -4,6 +4,7 @@
<user-teaser
:user="isGroup ? notification.relatedUser : from.author"
:date-time="from.createdAt"
:show-popover="false"
/>
</client-only>
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>

View File

@ -127,7 +127,7 @@ export default {
apollo: {
notifications: {
query() {
return notificationQuery(this.$i18n)
return notificationQuery()
},
variables() {
return {

View File

@ -88,7 +88,7 @@ describe('NotificationsTable.vue', () => {
})
it('renders the author', () => {
const userinfo = firstRowNotification.find('.user-teaser > .info')
const userinfo = firstRowNotification.find('.user-teaser .info')
expect(userinfo.text()).toContain(postNotification.from.author.name)
})
@ -121,7 +121,7 @@ describe('NotificationsTable.vue', () => {
})
it('renders the author', () => {
const userinfo = secondRowNotification.find('.user-teaser > .info')
const userinfo = secondRowNotification.find('.user-teaser .info')
expect(userinfo.text()).toContain(commentNotification.from.author.name)
})

View File

@ -59,7 +59,12 @@ describe('Request', () => {
})
it('delivers email to backend', () => {
const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } })
const expected = expect.objectContaining({
variables: {
email: 'mail@example.org',
locale: 'en',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
@ -92,7 +97,12 @@ describe('Request', () => {
})
it('normalizes email to lower case letters', () => {
const expected = expect.objectContaining({ variables: { email: 'mail@gmail.com' } })
const expected = expect.objectContaining({
variables: {
email: 'mail@gmail.com',
locale: 'en',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
})

View File

@ -85,13 +85,13 @@ export default {
},
async handleSubmit() {
const mutation = gql`
mutation ($email: String!) {
requestPasswordReset(email: $email)
mutation ($email: String!, $locale: String!) {
requestPasswordReset(email: $email, locale: $locale)
}
`
try {
const { email } = this
await this.$apollo.mutate({ mutation, variables: { email } })
await this.$apollo.mutate({ mutation, variables: { email, locale: this.$i18n.locale() } })
this.submitted = true
setTimeout(() => {

View File

@ -36,8 +36,8 @@ import normalizeEmail from '~/components/utils/NormalizeEmail'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
export const SignupMutation = gql`
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
mutation ($email: String!, $locale: String!, $inviteCode: String) {
Signup(email: $email, locale: $locale, inviteCode: $inviteCode) {
email
}
}
@ -140,7 +140,7 @@ export default {
async onNextClick() {
const { email } = this.formData
const { inviteCode = null } = this.sliderData.collectedInputData
const variables = { email, inviteCode }
const variables = { email, inviteCode, locale: this.$i18n.locale() }
if (this.sliderData.collectedInputData.emailSend && !this.sendEmailAgain) {
return true

View File

@ -25,6 +25,9 @@ describe('Signup', () => {
loading: false,
mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }),
},
$i18n: {
locale: () => 'de',
},
}
propsData = {}
})
@ -64,7 +67,7 @@ describe('Signup', () => {
it('delivers email to backend', () => {
const expected = expect.objectContaining({
mutation: SignupMutation,
variables: { email: 'mAIL@exAMPLE.org', inviteCode: null },
variables: { email: 'mAIL@exAMPLE.org', locale: 'de', inviteCode: null },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})

View File

@ -70,8 +70,8 @@ import { SweetalertIcon } from 'vue-sweetalert-icons'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
export const SignupMutation = gql`
mutation ($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
mutation ($email: String!, $locale: String!, $inviteCode: String) {
Signup(email: $email, locale: $locale, inviteCode: $inviteCode) {
email
}
}
@ -121,7 +121,7 @@ export default {
try {
const response = await this.$apollo.mutate({
mutation: SignupMutation,
variables: { email, inviteCode: null },
variables: { email, locale: this.$i18n.locale(), inviteCode: null },
})
this.data = response.data
setTimeout(() => {

View File

@ -0,0 +1,31 @@
import { render } from '@testing-library/vue'
import LocationInfo from './LocationInfo.vue'
const localVue = global.localVue
describe('LocationInfo', () => {
const Wrapper = ({ withDistance }) => {
return render(LocationInfo, {
localVue,
propsData: {
locationData: {
name: 'Paris',
distanceToMe: withDistance ? 100 : null,
},
},
mocks: {
$t: jest.fn((t) => t),
},
})
}
it('renders with distance', () => {
const wrapper = Wrapper({ withDistance: true })
expect(wrapper.container).toMatchSnapshot()
})
it('renders without distance', () => {
const wrapper = Wrapper({ withDistance: false })
expect(wrapper.container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,44 @@
<template>
<div class="location-info">
<div class="location">
<base-icon name="map-marker" />
{{ locationData.name }}
</div>
<div v-if="distance" class="distance">{{ distance }}</div>
</div>
</template>
<script>
export default {
name: 'LocationInfo',
props: {
locationData: { type: Object, default: null },
},
computed: {
distance() {
return this.locationData.distanceToMe === null
? null
: this.$t('location.distance', { distance: this.locationData.distanceToMe })
},
},
}
</script>
<style scoped>
.location-info {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.location {
display: flex;
align-items: center;
justify-content: center;
}
.distance {
margin-top: 8px;
}
}
</style>

View File

@ -1,113 +1,250 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import { render, screen, fireEvent } from '@testing-library/vue'
import { RouterLinkStub } from '@vue/test-utils'
import UserTeaser from './UserTeaser.vue'
import Vuex from 'vuex'
const localVue = global.localVue
const filter = jest.fn((str) => str)
localVue.filter('truncate', filter)
// Mock Math.random, used in Dropdown
Object.assign(Math, {
random: () => 0,
})
const waitForPopover = async () => await new Promise((resolve) => setTimeout(resolve, 1000))
let mockIsTouchDevice
jest.mock('../utils/isTouchDevice', () => ({
isTouchDevice: jest.fn(() => mockIsTouchDevice),
}))
const userTilda = {
name: 'Tilda Swinton',
slug: 'tilda-swinton',
id: 'user1',
avatar: '/avatars/tilda-swinton',
badgeVerification: {
id: 'bv1',
icon: '/icons/verified',
description: 'Verified',
isDefault: false,
},
badgeTrophiesSelected: [
{
id: 'trophy1',
icon: '/icons/trophy1',
description: 'Trophy 1',
isDefault: false,
},
{
id: 'trophy2',
icon: '/icons/trophy2',
description: 'Trophy 2',
isDefault: false,
},
{
id: 'empty',
icon: '/icons/empty',
description: 'Empty',
isDefault: true,
},
],
}
describe('UserTeaser', () => {
let propsData
let mocks
let stubs
let getters
const Wrapper = ({
isModerator = false,
withLinkToProfile = true,
onTouchScreen = false,
withAvatar = true,
user = userTilda,
withPopoverEnabled = true,
}) => {
mockIsTouchDevice = onTouchScreen
beforeEach(() => {
propsData = {}
mocks = {
$t: jest.fn(),
}
stubs = {
NuxtLink: RouterLinkStub,
}
getters = {
'auth/user': () => {
return {}
const store = new Vuex.Store({
getters: {
'auth/user': () => {
return {}
},
'auth/isModerator': () => isModerator,
},
'auth/isModerator': () => false,
}
})
return render(UserTeaser, {
localVue,
store,
propsData: {
user,
linkToProfile: withLinkToProfile,
showAvatar: withAvatar,
showPopover: withPopoverEnabled,
},
stubs: {
NuxtLink: RouterLinkStub,
'user-teaser-popover': true,
'v-popover': true,
'client-only': true,
},
mocks: {
$t: jest.fn((t) => t),
},
})
}
it('renders anonymous user', () => {
const wrapper = Wrapper({ user: null })
expect(wrapper.container).toMatchSnapshot()
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters,
describe('given an user', () => {
describe('without linkToProfile, on touch screen', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: true })
})
return mount(UserTeaser, { store, propsData, mocks, stubs, localVue })
}
it('renders anonymous user', () => {
const wrapper = Wrapper()
expect(wrapper.text()).toBe('')
expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
it('renders', () => {
expect(wrapper.container).toMatchSnapshot()
})
describe('when clicking the user name', () => {
beforeEach(async () => {
const userName = screen.getByText('Tilda Swinton')
await fireEvent.click(userName)
await waitForPopover()
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
describe('when clicking the user avatar', () => {
beforeEach(async () => {
const userAvatar = screen.getByAltText('Tilda Swinton')
await fireEvent.click(userAvatar)
await waitForPopover()
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
})
describe('given an user', () => {
describe('with linkToProfile, on touch screen', () => {
let wrapper
beforeEach(() => {
propsData.user = {
name: 'Tilda Swinton',
slug: 'tilda-swinton',
}
wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: true })
})
it('renders user name', () => {
const wrapper = Wrapper()
expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym')
expect(wrapper.text()).toMatch('Tilda Swinton')
it('renders', () => {
expect(wrapper.container).toMatchSnapshot()
})
describe('user is deleted', () => {
beforeEach(() => {
propsData.user.deleted = true
describe('when clicking the user name', () => {
beforeEach(async () => {
const userName = screen.getByText('Tilda Swinton')
await fireEvent.click(userName)
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
})
describe('without linkToProfile, on desktop', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: false })
})
it('renders', () => {
expect(wrapper.container).toMatchSnapshot()
})
describe('when hovering the user name', () => {
beforeEach(async () => {
const userName = screen.getByText('Tilda Swinton')
await fireEvent.mouseOver(userName)
await waitForPopover()
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
describe('when hovering the user avatar', () => {
beforeEach(async () => {
const userAvatar = screen.getByAltText('Tilda Swinton')
await fireEvent.mouseOver(userAvatar)
await waitForPopover()
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
})
describe('with linkToProfile, on desktop', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: false })
})
it('renders', () => {
expect(wrapper.container).toMatchSnapshot()
})
describe('when hovering the user name', () => {
beforeEach(async () => {
const userName = screen.getByText('Tilda Swinton')
await fireEvent.mouseOver(userName)
await waitForPopover()
})
it('renders the popover', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
})
describe('avatar is disabled', () => {
it('does not render the avatar', () => {
const wrapper = Wrapper({ withAvatar: false })
expect(wrapper.container).toMatchSnapshot()
})
})
describe('user is deleted', () => {
it('renders anonymous user', () => {
const wrapper = Wrapper({ user: { ...userTilda, deleted: true } })
expect(wrapper.container).toMatchSnapshot()
})
describe('even if the current user is a moderator', () => {
it('renders anonymous user', () => {
const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Tilda Swinton')
expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
})
describe('even if the current user is a moderator', () => {
beforeEach(() => {
getters['auth/isModerator'] = () => true
})
it('renders anonymous user', () => {
const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Tilda Swinton')
expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
const wrapper = Wrapper({
user: { ...userTilda, deleted: true },
isModerator: true,
})
expect(wrapper.container).toMatchSnapshot()
})
})
})
describe('user is disabled', () => {
beforeEach(() => {
propsData.user.disabled = true
})
describe('user is disabled', () => {
it('renders anonymous user', () => {
const wrapper = Wrapper({ user: { ...userTilda, disabled: true } })
expect(wrapper.container).toMatchSnapshot()
})
it('renders anonymous user', () => {
const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Tilda Swinton')
expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
})
describe('current user is a moderator', () => {
beforeEach(() => {
getters['auth/isModerator'] = () => true
})
it('renders user name', () => {
const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Anonymous')
expect(wrapper.text()).toMatch('Tilda Swinton')
})
it('has "disabled-content" class', () => {
const wrapper = Wrapper()
expect(wrapper.classes()).toContain('disabled-content')
})
describe('current user is a moderator', () => {
it('renders user name', () => {
const wrapper = Wrapper({ user: { ...userTilda, disabled: true }, isModerator: true })
expect(wrapper.container).toMatchSnapshot()
})
})
})

View File

@ -4,57 +4,34 @@
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
</div>
<div v-else :class="[{ 'disabled-content': user.disabled }]" placement="top-start">
<div :class="['user-teaser']">
<nuxt-link v-if="linkToProfile && showAvatar" :to="userLink" data-test="avatarUserLink">
<profile-avatar :profile="user" size="small" />
</nuxt-link>
<profile-avatar v-else-if="showAvatar" :profile="user" size="small" />
<div class="info flex-direction-column">
<div :class="wide ? 'flex-direction-row' : 'flex-direction-column'">
<nuxt-link v-if="linkToProfile" :to="userLink">
<span class="text">
<span class="slug">{{ userSlug }}</span>
<span class="name">{{ userName }}</span>
</span>
</nuxt-link>
<span v-else class="text">
<span class="slug">{{ userSlug }}</span>
<span class="name">{{ userName }}</span>
</span>
<span v-if="wide">&nbsp;</span>
<span v-if="group">
<span class="text">
{{ $t('group.in') }}
</span>
<nuxt-link :to="groupLink">
<span class="text">
<span class="slug">{{ groupSlug }}</span>
<span v-if="!userOnly" class="name">{{ groupName }}</span>
</span>
</nuxt-link>
</span>
</div>
<span v-if="!userOnly && dateTime" class="text">
<base-icon name="clock" />
<date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
</span>
</div>
</div>
<!-- isTouchDevice only supported on client-->
<client-only>
<user-teaser-non-anonymous
v-if="user"
:link-to-profile="linkToProfile"
:user="user"
:group="group"
:wide="wide"
:show-avatar="showAvatar"
:date-time="dateTime"
:show-popover="showPopover"
@close="closeMenu"
/>
</client-only>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import DateTime from '~/components/DateTime'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import UserTeaserNonAnonymous from './UserTeaserNonAnonymous'
export default {
name: 'UserTeaser',
components: {
DateTime,
ProfileAvatar,
UserTeaserNonAnonymous,
},
props: {
linkToProfile: { type: Boolean, default: true },
@ -69,71 +46,36 @@ export default {
...mapGetters({
isModerator: 'auth/isModerator',
}),
itsMe() {
return this.user.slug === this.$store.getters['auth/user'].slug
},
displayAnonymous() {
const { user, isModerator } = this
return !user || user.deleted || (user.disabled && !isModerator)
},
userLink() {
const { id, slug } = this.user
if (!(id && slug)) return ''
return { name: 'profile-id-slug', params: { slug, id } }
},
userSlug() {
const { slug } = this.user || {}
return slug && `@${slug}`
},
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
userOnly() {
return !this.dateTime && !this.group
},
groupLink() {
const { id, slug } = this.group
if (!(id && slug)) return ''
return { name: 'groups-id-slug', params: { slug, id } }
},
groupSlug() {
const { slug } = this.group || {}
return slug && `&${slug}`
},
groupName() {
const { name } = this.group || {}
return name || this.$t('profile.userAnonym')
},
},
methods: {
optimisticFollow({ followedByCurrentUser }) {
const inc = followedByCurrentUser ? 1 : -1
this.user.followedByCurrentUser = followedByCurrentUser
this.user.followedByCount += inc
},
updateFollow({ followedByCurrentUser, followedByCount }) {
this.user.followedByCount = followedByCount
this.user.followedByCurrentUser = followedByCurrentUser
closeMenu() {
this.$emit('close')
},
},
}
</script>
<style lang="scss">
.trigger {
max-width: 100%;
}
.user-teaser {
display: flex;
flex-wrap: nowrap;
> .profile-avatar {
.trigger {
max-width: 100%;
display: flex !important;
justify-content: center;
align-items: center;
}
.profile-avatar {
flex-shrink: 0;
}
> .info {
.info {
padding-left: $space-xx-small;
overflow: hidden;
text-overflow: ellipsis;
@ -145,12 +87,12 @@ export default {
.slug {
color: $color-primary;
font-size: $font-size-base;
font-size: calc(1.15 * $font-size-base);
}
.name {
color: $text-color-soft;
font-size: $font-size-small;
font-size: $font-size-base;
}
}
@ -158,12 +100,14 @@ export default {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.flex-direction-row {
display: flex;
flex-direction: row;
justify-content: center;
padding-left: 2px;
}
.text {

View File

@ -0,0 +1,71 @@
import { render } from '@testing-library/vue'
import { RouterLinkStub } from '@vue/test-utils'
import UserTeaserHelper from './UserTeaserHelper.vue'
const localVue = global.localVue
const userLink = {
name: 'profile-id-slug',
params: { slug: 'slug', id: 'id' },
}
let mockIsTouchDevice
jest.mock('../utils/isTouchDevice', () => ({
isTouchDevice: jest.fn(() => mockIsTouchDevice),
}))
describe('UserTeaserHelper', () => {
const Wrapper = ({
withLinkToProfile = true,
onTouchScreen = false,
withPopoverEnabled = true,
}) => {
mockIsTouchDevice = onTouchScreen
return render(UserTeaserHelper, {
localVue,
propsData: {
userLink,
linkToProfile: withLinkToProfile,
showPopover: withPopoverEnabled,
},
stubs: {
NuxtLink: RouterLinkStub,
},
})
}
describe('with linkToProfile and popover enabled, on touch screen', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: true, withPopoverEnabled: true })
})
it('renders button', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
describe('without linkToProfile', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: false, onTouchScreen: false })
})
it('renders span', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
describe('with linkToProfile, on desktop', () => {
let wrapper
beforeEach(() => {
wrapper = Wrapper({ withLinkToProfile: true, onTouchScreen: false })
})
it('renders link', () => {
expect(wrapper.container).toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,46 @@
<template>
<button v-if="showPopover && isTouchDevice" @click.prevent="openMenu">
<slot />
</button>
<span
v-else-if="!linkToProfile || !userLink"
@mouseover="() => showPopover && openMenu()"
@mouseleave="closeMenu"
>
<slot />
</span>
<nuxt-link
v-else
:to="userLink"
@mouseover.native="() => showPopover && openMenu()"
@mouseleave.native="closeMenu"
>
<slot />
</nuxt-link>
</template>
<script>
import { isTouchDevice } from '../utils/isTouchDevice'
export default {
name: 'UserTeaserHelper',
props: {
userLink: { type: Object, default: null },
linkToProfile: { type: Boolean, default: true },
showPopover: { type: Boolean, default: false },
},
computed: {
isTouchDevice() {
return isTouchDevice()
},
},
methods: {
openMenu() {
this.$emit('open-menu')
},
closeMenu() {
this.$emit('close-menu')
},
},
}
</script>

View File

@ -0,0 +1,123 @@
<template>
<dropdown class="user-teaser">
<template #default="{ openMenu, closeMenu }">
<user-teaser-helper
v-if="showAvatar"
:link-to-profile="linkToProfile"
:show-popover="showPopover"
:user-link="userLink"
@open-menu="openMenu(false)"
@close-menu="closeMenu(false)"
data-test="avatarUserLink"
>
<profile-avatar :profile="user" size="small" />
</user-teaser-helper>
<div class="info flex-direction-column">
<div :class="wide ? 'flex-direction-row' : 'flex-direction-column'">
<user-teaser-helper
:link-to-profile="linkToProfile"
:show-popover="showPopover"
:user-link="userLink"
@open-menu="openMenu(false)"
@close-menu="closeMenu(false)"
>
<span class="slug">{{ userSlug }}</span>
<span class="name">{{ userName }}</span>
</user-teaser-helper>
<span v-if="wide">&nbsp;</span>
<span v-if="group">
<span class="text">
{{ $t('group.in') }}
</span>
<nuxt-link :to="groupLink">
<span class="text">
<span class="slug">{{ groupSlug }}</span>
<span v-if="!userOnly" class="name">{{ groupName }}</span>
</span>
</nuxt-link>
</span>
</div>
<span v-if="!userOnly && dateTime" class="text">
<base-icon name="clock" />
<date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
</span>
</div>
</template>
<template #popover="{ isOpen }" v-if="showPopover">
<user-teaser-popover
v-if="isOpen"
:user-id="user.id"
:user-link="linkToProfile ? userLink : null"
/>
</template>
</dropdown>
</template>
<script>
import { mapGetters } from 'vuex'
import DateTime from '~/components/DateTime'
import Dropdown from '~/components/Dropdown'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import UserTeaserPopover from './UserTeaserPopover'
import UserTeaserHelper from './UserTeaserHelper.vue'
export default {
name: 'UserTeaser',
components: {
ProfileAvatar,
UserTeaserPopover,
UserTeaserHelper,
Dropdown,
DateTime,
},
props: {
linkToProfile: { type: Boolean, default: true },
user: { type: Object, default: null },
group: { type: Object, default: null },
wide: { type: Boolean, default: false },
showAvatar: { type: Boolean, default: true },
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
},
computed: {
...mapGetters({
isModerator: 'auth/isModerator',
}),
itsMe() {
return this.user.slug === this.$store.getters['auth/user'].slug
},
userLink() {
const { id, slug } = this.user
if (!(id && slug)) return null
return { name: 'profile-id-slug', params: { slug, id } }
},
userSlug() {
const { slug } = this.user || {}
return slug && `@${slug}`
},
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
userOnly() {
return !this.dateTime && !this.group
},
groupLink() {
const { id, slug } = this.group
if (!(id && slug)) return ''
return { name: 'groups-id-slug', params: { slug, id } }
},
groupSlug() {
const { slug } = this.group || {}
return slug && `&${slug}`
},
groupName() {
const { name } = this.group || {}
return name || this.$t('profile.userAnonym')
},
},
}
</script>

View File

@ -0,0 +1,99 @@
import { render } from '@testing-library/vue'
import { RouterLinkStub } from '@vue/test-utils'
import UserTeaserPopover from './UserTeaserPopover.vue'
const localVue = global.localVue
const user = {
id: 'id',
name: 'Tilda Swinton',
slug: 'tilda-swinton',
badgeVerification: {
id: 'bv1',
icon: '/icons/verified',
description: 'Verified',
isDefault: false,
},
badgeTrophiesSelected: [
{
id: 'trophy1',
icon: '/icons/trophy1',
description: 'Trophy 1',
isDefault: false,
},
{
id: 'trophy2',
icon: '/icons/trophy2',
description: 'Trophy 2',
isDefault: false,
},
{
id: 'empty',
icon: '/icons/empty',
description: 'Empty',
isDefault: true,
},
],
}
const userLink = {
name: 'profile-id-slug',
params: { slug: 'slug', id: 'id' },
}
describe('UserTeaserPopover', () => {
const Wrapper = ({ badgesEnabled = true, withUserLink = true, onTouchScreen = false }) => {
const mockIsTouchDevice = onTouchScreen
jest.mock('../utils/isTouchDevice', () => ({
isTouchDevice: jest.fn(() => mockIsTouchDevice),
}))
return render(UserTeaserPopover, {
localVue,
propsData: {
userId: 'id',
userLink: withUserLink ? userLink : null,
},
data: () => ({
User: [user],
}),
stubs: {
NuxtLink: RouterLinkStub,
},
mocks: {
$t: jest.fn((t) => t),
$env: {
BADGES_ENABLED: badgesEnabled,
},
},
})
}
describe('given a touch device', () => {
it('shows button when userLink is provided', () => {
const wrapper = Wrapper({ withUserLink: true, onTouchScreen: true })
expect(wrapper.container).toMatchSnapshot()
})
it('does not show button when userLink is not provided', () => {
const wrapper = Wrapper({ withUserLink: false, onTouchScreen: true })
expect(wrapper.container).toMatchSnapshot()
})
})
describe('given a non-touch device', () => {
it('does not show button when userLink is provided', () => {
const wrapper = Wrapper({ withUserLink: true, onTouchScreen: false })
expect(wrapper.container).toMatchSnapshot()
})
})
it('shows badges when enabled', () => {
const wrapper = Wrapper({ badgesEnabled: true })
expect(wrapper.container).toMatchSnapshot()
})
it('does not show badges when disabled', () => {
const wrapper = Wrapper({ badgesEnabled: false })
expect(wrapper.container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,103 @@
<template>
<div class="placeholder" v-if="!user" />
<div class="user-teaser-popover" v-else>
<badges
v-if="$env.BADGES_ENABLED && user.badgeVerification"
:badges="[user.badgeVerification, ...user.badgeTrophiesSelected]"
/>
<location-info v-if="user.location" :location-data="user.location" class="location-info" />
<ul class="statistics">
<li>
<ds-number :count="user.followedByCount" :label="$t('profile.followers')" />
</li>
<li>
<ds-number
:count="user.contributionsCount"
:label="$t('common.post', null, user.contributionsCount)"
/>
</li>
<li>
<ds-number
:count="user.commentedCount"
:label="$t('common.comment', null, user.commentedCount)"
/>
</li>
</ul>
<nuxt-link v-if="isTouchDevice && userLink" :to="userLink" class="link">
<ds-button primary>{{ $t('user-teaser.popover.open-profile') }}</ds-button>
</nuxt-link>
</div>
</template>
<script>
import Badges from '~/components/Badges.vue'
import LocationInfo from '~/components/UserTeaser/LocationInfo.vue'
import { isTouchDevice } from '~/components/utils/isTouchDevice'
import { userTeaserQuery } from '~/graphql/User.js'
export default {
name: 'UserTeaserPopover',
components: {
Badges,
LocationInfo,
},
props: {
userId: { type: String },
userLink: { type: Object },
},
computed: {
isTouchDevice() {
return isTouchDevice()
},
user() {
return (this.User && this.User[0]) ?? null
},
},
apollo: {
User: {
query() {
return userTeaserQuery(this.$i18n)
},
variables() {
return { id: this.userId }
},
},
},
}
</script>
<style scoped>
.placeholder {
height: 200px;
width: 200px;
}
.user-teaser-popover {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
gap: 16px;
min-width: 200px;
}
@media (max-height: 800px) {
.user-teaser-popover {
padding: 0;
gap: 0;
}
}
.location-info {
margin-bottom: 12px;
}
.link {
margin-top: 16px;
}
.statistics {
display: flex;
justify-content: space-between;
width: 100%;
}
</style>

View File

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationInfo renders with distance 1`] = `
<div>
<div
class="location-info"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
</div>
`;
exports[`LocationInfo renders without distance 1`] = `
<div>
<div
class="location-info"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<!---->
</div>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = `
<div>
<button />
</div>
`;
exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = `
<div>
<a />
</div>
`;
exports[`UserTeaserHelper without linkToProfile renders span 1`] = `
<div>
<span />
</div>
`;

View File

@ -0,0 +1,547 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserTeaserPopover does not show badges when disabled 1`] = `
<div>
<div
class="user-teaser-popover"
>
<!---->
<!---->
<ul
class="statistics"
>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
profile.followers
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.post
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.comment
</p>
</div>
</li>
</ul>
<!---->
</div>
</div>
`;
exports[`UserTeaserPopover given a non-touch device does not show button when userLink is provided 1`] = `
<div>
<div
class="user-teaser-popover"
>
<div
class="hc-badges"
>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/verified"
title="Verified"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy1"
title="Trophy 1"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy2"
title="Trophy 2"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/empty"
title="Empty"
/>
</div>
</div>
<!---->
<ul
class="statistics"
>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
profile.followers
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.post
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.comment
</p>
</div>
</li>
</ul>
<!---->
</div>
</div>
`;
exports[`UserTeaserPopover given a touch device does not show button when userLink is not provided 1`] = `
<div>
<div
class="user-teaser-popover"
>
<div
class="hc-badges"
>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/verified"
title="Verified"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy1"
title="Trophy 1"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy2"
title="Trophy 2"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/empty"
title="Empty"
/>
</div>
</div>
<!---->
<ul
class="statistics"
>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
profile.followers
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.post
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.comment
</p>
</div>
</li>
</ul>
<!---->
</div>
</div>
`;
exports[`UserTeaserPopover given a touch device shows button when userLink is provided 1`] = `
<div>
<div
class="user-teaser-popover"
>
<div
class="hc-badges"
>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/verified"
title="Verified"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy1"
title="Trophy 1"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy2"
title="Trophy 2"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/empty"
title="Empty"
/>
</div>
</div>
<!---->
<ul
class="statistics"
>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
profile.followers
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.post
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.comment
</p>
</div>
</li>
</ul>
<!---->
</div>
</div>
`;
exports[`UserTeaserPopover shows badges when enabled 1`] = `
<div>
<div
class="user-teaser-popover"
>
<div
class="hc-badges"
>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/verified"
title="Verified"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy1"
title="Trophy 1"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/trophy2"
title="Trophy 2"
/>
</div>
<div
class="hc-badge-container"
>
<img
class="hc-badge"
src="/api/icons/empty"
title="Empty"
/>
</div>
</div>
<!---->
<ul
class="statistics"
>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
profile.followers
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.post
</p>
</div>
</li>
<li>
<div
class="ds-number ds-number-size-x-large"
>
<p
class="ds-text ds-number-count ds-text-size-x-large"
style="margin-bottom: 0px;"
>
0
</p>
<p
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
>
common.comment
</p>
</div>
</li>
</ul>
<!---->
</div>
</div>
`;

View File

@ -0,0 +1,2 @@
export const isTouchDevice = () =>
'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0

View File

@ -17,9 +17,11 @@ export const locationFragment = (lang) => gql`
fragment location on User {
locationName
location {
id
name: name${lang}
lng
lat
distanceToMe
}
}
`
@ -50,6 +52,19 @@ export const userCountsFragment = gql`
}
`
export const userTeaserFragment = (lang) => gql`
${badgesFragment}
${locationFragment(lang)}
fragment userTeaser on User {
followedByCount
contributionsCount
commentedCount
...badges
...location
}
`
export const postFragment = gql`
fragment post on Post {
id

View File

@ -7,6 +7,7 @@ import {
postFragment,
commentFragment,
groupFragment,
userTeaserFragment,
} from './Fragments'
export const profileUserQuery = (i18n) => {
@ -125,7 +126,7 @@ export const mapUserQuery = (i18n) => {
`
}
export const notificationQuery = (_i18n) => {
export const notificationQuery = () => {
return gql`
${userFragment}
${commentFragment}
@ -483,6 +484,18 @@ export const userDataQuery = (i18n) => {
`
}
export const userTeaserQuery = (i18n) => {
const lang = i18n.locale().toUpperCase()
return gql`
${userTeaserFragment(lang)}
query ($id: ID!) {
User(id: $id) {
...userTeaser
}
}
`
}
export const setTrophyBadgeSelected = gql`
mutation ($slot: Int!, $badgeId: ID) {
setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) {

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": "Sprache wählen"
},
"location": {
"distance": "{distance} km von mir entfernt"
},
"login": {
"email": "Deine E-Mail",
"failure": "Fehlerhafte E-Mail-Adresse oder Passwort.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Neue Nutzungsbedingungen",
"termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
"termsAndConditionsNewConfirmText": "Bitte lies Dir die neuen Nutzungsbedingungen jetzt durch!"
},
"user-teaser": {
"popover": {
"open-profile": "Profil öffnen"
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": "Choose language"
},
"location": {
"distance": "{distance} km away from me"
},
"login": {
"email": "Your E-mail",
"failure": "Incorrect email address or password.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "New Terms and Conditions",
"termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
"termsAndConditionsNewConfirmText": "Please read the new terms of use now!"
},
"user-teaser": {
"popover": {
"open-profile": "Open profile"
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Su correo electrónico",
"failure": "Dirección de correo electrónico o contraseña incorrecta.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Nuevos términos de uso",
"termsAndConditionsNewConfirm": "He leído y acepto los nuevos términos de uso.",
"termsAndConditionsNewConfirmText": "¡Por favor, lea los nuevos términos de uso ahora!"
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Votre mail",
"failure": "Adresse mail ou mot de passe incorrect.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Nouvelles conditions générales",
"termsAndConditionsNewConfirm": "J'ai lu et accepté les nouvelles conditions générales.",
"termsAndConditionsNewConfirmText": "Veuillez lire les nouvelles conditions d'utilisation dès maintenant !"
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "La tua email",
"failure": null,
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Nuovi Termini e Condizioni",
"termsAndConditionsNewConfirm": "Ho letto e accetto le nuove condizioni generali di contratto.",
"termsAndConditionsNewConfirmText": "Si prega di leggere le nuove condizioni d'uso ora!"
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Uw E-mail",
"failure": null,
@ -1173,5 +1176,10 @@
"newTermsAndConditions": null,
"termsAndConditionsNewConfirm": null,
"termsAndConditionsNewConfirmText": null
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Twój adres e-mail",
"failure": null,
@ -1173,5 +1176,10 @@
"newTermsAndConditions": null,
"termsAndConditionsNewConfirm": null,
"termsAndConditionsNewConfirmText": null
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Seu email",
"failure": "Endereço de e-mail ou senha incorretos.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Novos Termos e Condições",
"termsAndConditionsNewConfirm": "Eu li e concordo com os novos termos de condições.",
"termsAndConditionsNewConfirmText": "Por favor, leia os novos termos de uso agora!"
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -636,6 +636,9 @@
"localeSwitch": {
"tooltip": null
},
"location": {
"distance": null
},
"login": {
"email": "Электронная почта",
"failure": "Неверный адрес электронной почты или пароль.",
@ -1173,5 +1176,10 @@
"newTermsAndConditions": "Новые условия и положения",
"termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.",
"termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!"
},
"user-teaser": {
"popover": {
"open-profile": null
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ocelot-social/maintenance",
"version": "3.4.0",
"version": "3.5.0",
"description": "Maintenance page for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-webapp",
"version": "3.4.0",
"version": "3.5.0",
"description": "ocelot.social Frontend",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",

File diff suppressed because it is too large Load Diff