mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #2870 from Human-Connection/migrate-styleguide-card
refactor: migrate card component
This commit is contained in:
commit
fa32c5789b
@ -30,7 +30,7 @@ Then("my comment should be successfully created", () => {
|
||||
});
|
||||
|
||||
Then("I should see my comment", () => {
|
||||
cy.get("div.comment p")
|
||||
cy.get("article.comment-card p")
|
||||
.should("contain", "Human Connection rocks")
|
||||
.get(".user-avatar img")
|
||||
.should("have.attr", "src")
|
||||
@ -40,12 +40,12 @@ Then("I should see my comment", () => {
|
||||
});
|
||||
|
||||
Then("I should see the entirety of my comment", () => {
|
||||
cy.get("div.comment")
|
||||
cy.get("article.comment-card")
|
||||
.should("not.contain", "show more")
|
||||
});
|
||||
|
||||
Then("I should see an abreviated version of my comment", () => {
|
||||
cy.get("div.comment")
|
||||
cy.get("article.comment-card")
|
||||
.should("contain", "show more")
|
||||
});
|
||||
|
||||
@ -60,7 +60,7 @@ Then("it should create a mention in the CommentForm", () => {
|
||||
})
|
||||
|
||||
When("I open the content menu of post {string}", (title)=> {
|
||||
cy.contains('.post-card', title)
|
||||
cy.contains('.post-teaser', title)
|
||||
.find('.content-menu .base-button')
|
||||
.click()
|
||||
})
|
||||
@ -77,9 +77,10 @@ Then("there is no button to pin a post", () => {
|
||||
})
|
||||
|
||||
And("the post with title {string} has a ribbon for pinned posts", (title) => {
|
||||
cy.get("article.post-card").contains(title)
|
||||
cy.get(".post-teaser").contains(title)
|
||||
.parent()
|
||||
.find("div.ribbon.ribbon--pinned")
|
||||
.parent()
|
||||
.find(".ribbon.--pinned")
|
||||
.should("contain", "Announcement")
|
||||
})
|
||||
|
||||
@ -111,7 +112,7 @@ Then("I add all required fields", () => {
|
||||
.get(".categories-select .base-button")
|
||||
.first()
|
||||
.click()
|
||||
.get('.ds-flex-item > .ds-form-item .ds-select ')
|
||||
.get('.base-card > .select-field input')
|
||||
.click()
|
||||
.get('.ds-select-option')
|
||||
.eq(languages.findIndex(l => l.code === 'en'))
|
||||
@ -119,7 +120,7 @@ Then("I add all required fields", () => {
|
||||
})
|
||||
|
||||
Then("the post was saved successfully with the {string} teaser image", condition => {
|
||||
cy.get(".ds-card-content > .ds-heading")
|
||||
cy.get(".base-card > .title")
|
||||
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||
.get(".content")
|
||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
||||
@ -128,25 +129,22 @@ Then("the post was saved successfully with the {string} teaser image", condition
|
||||
.and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
|
||||
})
|
||||
|
||||
Then("the first image should be removed from the preview", () => {
|
||||
cy.fixture("humanconnection.png").as('postTeaserImage').then(function() {
|
||||
cy.get("#postdropzone")
|
||||
.children()
|
||||
.get('img.thumbnail-preview')
|
||||
.should('have.length', 1)
|
||||
.and('have.attr', 'src')
|
||||
.and('contain', this.postTeaserImage)
|
||||
})
|
||||
Then("the first image should not be displayed anymore", () => {
|
||||
cy.get(".hero-image")
|
||||
.children()
|
||||
.get('.hero-image > .image')
|
||||
.should('have.length', 1)
|
||||
.and('have.attr', 'src')
|
||||
})
|
||||
|
||||
Then('the {string} post was saved successfully without a teaser image', condition => {
|
||||
cy.get(".ds-card-content > .ds-heading")
|
||||
cy.get(".base-card > .title")
|
||||
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||
.get(".content")
|
||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
||||
.get('.post-page')
|
||||
.should('exist')
|
||||
.get('.post-page img.ds-card-image')
|
||||
.get('.hero-image > .image')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
@ -156,12 +154,12 @@ Then('I should be able to remove it', () => {
|
||||
})
|
||||
|
||||
When('my post has a teaser image', () => {
|
||||
cy.get('.contribution-image')
|
||||
cy.get('.contribution-form .image')
|
||||
.should('exist')
|
||||
.and('have.attr', 'src')
|
||||
})
|
||||
|
||||
Then('I should be able to remove the image', () => {
|
||||
cy.get('.delete-image')
|
||||
cy.get('.dz-message > .base-button')
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,7 +29,7 @@ When("I visit another user's profile page", () => {
|
||||
});
|
||||
|
||||
Then("I cannot upload a picture", () => {
|
||||
cy.get(".ds-card-content")
|
||||
cy.get(".base-card")
|
||||
.children()
|
||||
.should("not.have.id", "customdropzone")
|
||||
.should("have.class", "user-avatar");
|
||||
|
||||
@ -12,7 +12,7 @@ let annoyingUserWhoMutedModeratorTitle = 'Fake news'
|
||||
const savePostTitle = $post => {
|
||||
return $post
|
||||
.first()
|
||||
.find('.ds-heading')
|
||||
.find('.title')
|
||||
.first()
|
||||
.invoke('text')
|
||||
.then(title => {
|
||||
@ -51,7 +51,7 @@ Given('I am logged in with a {string} role', role => {
|
||||
})
|
||||
|
||||
When('I click on "Report Post" from the content menu of the post', () => {
|
||||
cy.contains('.ds-card', davidIrvingPostTitle)
|
||||
cy.contains('.base-card', davidIrvingPostTitle)
|
||||
.find('.content-menu .base-button')
|
||||
.click({force: true})
|
||||
|
||||
@ -61,7 +61,7 @@ When('I click on "Report Post" from the content menu of the post', () => {
|
||||
})
|
||||
|
||||
When('I click on "Report User" from the content menu in the user info box', () => {
|
||||
cy.contains('.ds-card', davidIrvingPostTitle)
|
||||
cy.contains('.base-card', davidIrvingPostTitle)
|
||||
.get('.user-content-menu .base-button')
|
||||
.click({ force: true })
|
||||
|
||||
@ -78,7 +78,7 @@ When('I click on the author', () => {
|
||||
|
||||
When('I report the author', () => {
|
||||
cy.get('.page-name-profile-id-slug').then(() => {
|
||||
invokeReportOnElement('.ds-card').then(() => {
|
||||
invokeReportOnElement('.base-card').then(() => {
|
||||
cy.get('button')
|
||||
.contains('Send')
|
||||
.click()
|
||||
@ -169,7 +169,7 @@ Then('each list item links to the post page', () => {
|
||||
Then('I can visit the post page', () => {
|
||||
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
|
||||
cy.location('pathname').should('contain', '/post')
|
||||
.get('h3').should('contain', annoyingUserWhoMutedModeratorTitle)
|
||||
.get('title').should('contain', annoyingUserWhoMutedModeratorTitle)
|
||||
})
|
||||
|
||||
When("they have a post someone has reported", () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||
When("I search for {string}", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value);
|
||||
});
|
||||
@ -25,7 +25,7 @@ Then("the search should contain the annoying user", () => {
|
||||
expect($li).to.have.length(1);
|
||||
})
|
||||
cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type("{esc}");
|
||||
})
|
||||
@ -44,21 +44,21 @@ Then("I should see the following users in the select dropdown:", table => {
|
||||
});
|
||||
|
||||
When("I type {string} and press Enter", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value)
|
||||
.type("{enter}", { force: true });
|
||||
});
|
||||
|
||||
When("I type {string} and press escape", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value)
|
||||
.type("{esc}");
|
||||
});
|
||||
|
||||
Then("the search field should clear", () => {
|
||||
cy.get(".searchable-input .ds-select-search").should("have.text", "");
|
||||
cy.get(".searchable-input .ds-select input").should("have.text", "");
|
||||
});
|
||||
|
||||
When("I select a post entry", () => {
|
||||
|
||||
@ -80,7 +80,7 @@ Then('I should be on the {string} page', page => {
|
||||
.should(loc => {
|
||||
expect(loc.pathname).to.eq(page)
|
||||
})
|
||||
.get('h3')
|
||||
.get('h2')
|
||||
.should('contain', 'Social media')
|
||||
})
|
||||
|
||||
@ -112,7 +112,7 @@ Given('I have added a social media link', () => {
|
||||
})
|
||||
|
||||
Then('they should be able to see my social media links', () => {
|
||||
cy.get('.ds-card-content')
|
||||
cy.get('.base-card')
|
||||
.contains('Where else can I find Peter Pan?')
|
||||
.get('a[href="https://freeradical.zone/peter-pan"]')
|
||||
.should('have.length', 1)
|
||||
|
||||
@ -73,7 +73,7 @@ Given("the {string} user searches for {string}", (_, postTitle) => {
|
||||
})
|
||||
})
|
||||
.then(user => cy.login(user))
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(postTitle);
|
||||
});
|
||||
@ -295,14 +295,14 @@ Then("I select a category", () => {
|
||||
});
|
||||
|
||||
When("I choose {string} as the language for the post", (languageCode) => {
|
||||
cy.get('.ds-flex-item > .ds-form-item .ds-select ')
|
||||
cy.get('.contribution-form .ds-select')
|
||||
.click().get('.ds-select-option')
|
||||
.eq(languages.findIndex(l => l.code === languageCode)).click()
|
||||
})
|
||||
|
||||
Then("the post shows up on the landing page at position {int}", index => {
|
||||
cy.openPage("landing");
|
||||
const selector = `.post-card:nth-child(${index}) > .ds-card-content`;
|
||||
const selector = `.post-teaser:nth-child(${index}) > .base-card`;
|
||||
cy.get(selector).should("contain", lastPost.title);
|
||||
cy.get(selector).should("contain", lastPost.content);
|
||||
});
|
||||
@ -312,16 +312,16 @@ Then("I get redirected to {string}", route => {
|
||||
});
|
||||
|
||||
Then("the post was saved successfully", () => {
|
||||
cy.get(".ds-card-content > .ds-heading").should("contain", lastPost.title);
|
||||
cy.get(".base-card > .title").should("contain", lastPost.title);
|
||||
cy.get(".content").should("contain", lastPost.content);
|
||||
});
|
||||
|
||||
Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
|
||||
cy.get(".post-card").should("have.length", postCount);
|
||||
cy.get(".post-teaser").should("have.length", postCount);
|
||||
});
|
||||
|
||||
Then("the first post on the landing page has the title:", title => {
|
||||
cy.get(".post-card:first").should("contain", title);
|
||||
cy.get(".post-teaser:first").should("contain", title);
|
||||
});
|
||||
|
||||
Then(
|
||||
@ -388,7 +388,7 @@ Then("I can login successfully with password {string}", password => {
|
||||
|
||||
When("open the notification menu and click on the first item", () => {
|
||||
cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu
|
||||
cy.get(".notification-mention-post")
|
||||
cy.get(".notification .link")
|
||||
.first()
|
||||
.click({
|
||||
force: true
|
||||
@ -424,7 +424,7 @@ When("mention {string} in the text", mention => {
|
||||
Then("the notification gets marked as read", () => {
|
||||
cy.get(".notifications-menu-popover .notification")
|
||||
.first()
|
||||
.should("have.class", "read");
|
||||
.should("have.class", "--read");
|
||||
});
|
||||
|
||||
Then("there are no notifications in the top menu", () => {
|
||||
@ -510,14 +510,14 @@ Given('{string} wrote a post {string}', (_, title) => {
|
||||
});
|
||||
|
||||
Then("the list of posts of this user is empty", () => {
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".base-card").not(".post-link");
|
||||
cy.get(".main-container").find(".ds-space.hc-empty");
|
||||
});
|
||||
|
||||
Then("I get removed from his follower collection", () => {
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".base-card").not(".post-link");
|
||||
cy.get(".main-container").contains(
|
||||
".ds-card-content",
|
||||
".base-card",
|
||||
"is not followed by anyone"
|
||||
);
|
||||
});
|
||||
@ -581,7 +581,7 @@ Then("I see only one post with the title {string}", title => {
|
||||
});
|
||||
|
||||
Then("they should not see the comment form", () => {
|
||||
cy.get(".ds-card-footer").children().should('not.have.class', 'comment-form')
|
||||
cy.get(".base-card").children().should('not.have.class', 'comment-form')
|
||||
})
|
||||
|
||||
Then("they should see a text explaining why commenting is not possible", () => {
|
||||
@ -600,11 +600,11 @@ Then("I {string} see {string} from the content menu in the user info box", (cond
|
||||
})
|
||||
|
||||
Then('I should not see {string} button', button => {
|
||||
cy.get('.ds-card-content .action-buttons')
|
||||
cy.get('.base-card .action-buttons')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
|
||||
Then('I should see the {string} button', button => {
|
||||
cy.get('.ds-card-content .action-buttons .base-button')
|
||||
cy.get('.base-card .action-buttons .base-button')
|
||||
.should('contain', button)
|
||||
})
|
||||
|
||||
@ -35,7 +35,7 @@ Feature: Upload Teaser Image
|
||||
And confirm crop
|
||||
And I should be able to "change" a teaser image
|
||||
And confirm crop
|
||||
And the first image should be removed from the preview
|
||||
And the first image should not be displayed anymore
|
||||
|
||||
Scenario: Add image, then delete it
|
||||
When I click on the big plus icon in the bottom right corner to create post
|
||||
@ -44,4 +44,4 @@ Feature: Upload Teaser Image
|
||||
And I add all required fields
|
||||
And I click on "Save"
|
||||
Then I get redirected to ".../new-post"
|
||||
And the "new" post was saved successfully without a teaser image
|
||||
And the "new" post was saved successfully without a teaser image
|
||||
|
||||
@ -55,6 +55,6 @@ Feature: Block a User
|
||||
Scenario: Blocked users should not see link or button to unblock, only blocking users
|
||||
Given a user has blocked me
|
||||
When I visit the profile page of the annoying user
|
||||
And I "should not" see "Unblock user" from the content menu in the user info box
|
||||
And I should see the "Follow" button
|
||||
And I should not see "Unblock user" button
|
||||
And I should not see "Unblock user" button
|
||||
And I "should not" see "Unblock user" from the content menu in the user info box
|
||||
|
||||
@ -9,3 +9,13 @@ button {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -211,7 +211,8 @@ $letter-spacing-x-small: -0.015em;
|
||||
* @presenter Opacity
|
||||
*/
|
||||
|
||||
$opacity-soft: 0.65;
|
||||
$opacity-base: 1;
|
||||
$opacity-soft: 0.7;
|
||||
$opacity-disabled: 0.5;
|
||||
|
||||
/**
|
||||
@ -264,12 +265,23 @@ $size-avatar-large: 114px;
|
||||
$size-button-base: 36px;
|
||||
$size-button-small: 26px;
|
||||
|
||||
/**
|
||||
* @tokens Size Images
|
||||
* @presenter Spacing
|
||||
*/
|
||||
|
||||
$size-image-max-height: 2000px;
|
||||
$size-image-cropper-max-height: 600px;
|
||||
$size-image-cropper-min-height: 400px;
|
||||
$size-image-uploader-min-height: 200px;
|
||||
|
||||
/**
|
||||
* @tokens Size Icons
|
||||
* @presenter Spacing
|
||||
*/
|
||||
|
||||
$size-icon-base: 16px;
|
||||
$size-icon-large: 60px;
|
||||
|
||||
/**
|
||||
* @tokens Shadow
|
||||
@ -285,6 +297,12 @@ $box-shadow-active: 0 0 6px 1px rgba(20, 100, 160, 0.5);
|
||||
$box-shadow-inset: inset 0 0 20px 1px rgba(0,0,0,.15);
|
||||
$box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05);
|
||||
|
||||
/**
|
||||
* @tokens Effects
|
||||
*/
|
||||
|
||||
$blur-radius: 22px;
|
||||
|
||||
/**
|
||||
* @tokens Animation Duration
|
||||
*/
|
||||
@ -316,7 +334,8 @@ $z-index-page-submenu: 2500;
|
||||
$z-index-page-header: 2000;
|
||||
$z-index-page-sidebar: 1500;
|
||||
$z-index-sticky: 100;
|
||||
$z-index-post-card-link: 5;
|
||||
$z-index-post-teaser-link: 5;
|
||||
$z-index-surface: 1;
|
||||
|
||||
/**
|
||||
* @tokens Media Query
|
||||
|
||||
@ -13,6 +13,8 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
@ -141,10 +143,9 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.ds-card .ds-section {
|
||||
.base-card > .ds-section {
|
||||
padding: 0;
|
||||
margin-left: -$space-base;
|
||||
margin-right: -$space-base;
|
||||
margin: -$space-base;
|
||||
|
||||
.ds-container {
|
||||
padding: $space-base;
|
||||
|
||||
@ -1,30 +1,18 @@
|
||||
<template>
|
||||
<div class="categories-select">
|
||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
|
||||
<div v-for="category in categories" :key="category.id">
|
||||
<ds-flex-item>
|
||||
<base-button
|
||||
:data-test="categoryButtonsId(category.id)"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="isActive(category.id)"
|
||||
:disabled="isDisabled(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</ds-flex-item>
|
||||
</div>
|
||||
</ds-flex>
|
||||
<p class="small-info">
|
||||
{{
|
||||
$t('contribution.categories.infoSelectedNoOfMaxCategories', {
|
||||
chosen: selectedCount,
|
||||
max: selectedMax,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<section class="categories-select">
|
||||
<base-button
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:data-test="categoryButtonsId(category.id)"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="isActive(category.id)"
|
||||
:disabled="isDisabled(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -85,3 +73,15 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.categories-select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .base-button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,230 +0,0 @@
|
||||
<template>
|
||||
<div v-if="(comment.deleted || comment.disabled) && !isModerator" :class="{ comment: true }">
|
||||
<ds-card>
|
||||
<ds-space margin-bottom="base" />
|
||||
<ds-text style="padding-left: 40px; font-weight: bold;" color="soft">
|
||||
<base-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
</ds-text>
|
||||
<ds-space margin-bottom="base" />
|
||||
</ds-card>
|
||||
</div>
|
||||
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
||||
<ds-card :id="anchor" :class="{ 'comment--target': isTarget }">
|
||||
<ds-space margin-bottom="small" margin-top="small">
|
||||
<user-teaser :user="author" :date-time="comment.createdAt">
|
||||
<template v-slot:dateTime>
|
||||
<ds-text v-if="comment.createdAt !== comment.updatedAt">
|
||||
({{ $t('comment.edited') }})
|
||||
</ds-text>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
v-show="!openEditCommentMenu"
|
||||
placement="bottom-end"
|
||||
resource-type="comment"
|
||||
:resource="comment"
|
||||
:modalsData="menuModalsData"
|
||||
class="float-right"
|
||||
:is-owner="isAuthor(author.id)"
|
||||
@showEditCommentMenu="editCommentMenu"
|
||||
/>
|
||||
</client-only>
|
||||
</ds-space>
|
||||
<div v-if="openEditCommentMenu">
|
||||
<comment-form
|
||||
:update="true"
|
||||
:post="post"
|
||||
:comment="comment"
|
||||
@showEditCommentMenu="editCommentMenu"
|
||||
@updateComment="updateComment"
|
||||
@collapse="isCollapsed = true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<content-viewer :content="commentContent" class="comment-content" />
|
||||
<button
|
||||
v-if="isLongComment"
|
||||
type="button"
|
||||
class="collapse-button"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
>
|
||||
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
|
||||
</button>
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
size="small"
|
||||
v-scroll-to="'.editor'"
|
||||
@click.prevent="reply"
|
||||
></base-button>
|
||||
</ds-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
data() {
|
||||
const anchor = `commentId-${this.comment.id}`
|
||||
const isTarget = this.routeHash === `#${anchor}`
|
||||
|
||||
return {
|
||||
anchor,
|
||||
isTarget,
|
||||
isCollapsed: !isTarget,
|
||||
openEditCommentMenu: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserTeaser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
CommentForm,
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
routeHash: { type: String, default: () => '' },
|
||||
post: { type: Object, default: () => ({}) },
|
||||
comment: { type: Object, default: () => ({}) },
|
||||
dateTime: { type: [Date, String], default: null },
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
isModerator: 'auth/isModerator',
|
||||
}),
|
||||
isLongComment() {
|
||||
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
|
||||
},
|
||||
commentContent() {
|
||||
if (this.isLongComment && this.isCollapsed) {
|
||||
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
|
||||
}
|
||||
return this.comment.content
|
||||
},
|
||||
displaysComment() {
|
||||
return !this.unavailable || this.isModerator
|
||||
},
|
||||
author() {
|
||||
if (this.deleted) return {}
|
||||
return this.comment.author || {}
|
||||
},
|
||||
menuModalsData() {
|
||||
return {
|
||||
delete: {
|
||||
titleIdent: 'delete.comment.title',
|
||||
messageIdent: 'delete.comment.message',
|
||||
messageParams: {
|
||||
name: this.$filters.truncate(this.comment.contentExcerpt, 30),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'delete.submit',
|
||||
callback: this.deleteCommentCallback,
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'delete.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply() {
|
||||
const message = { slug: this.comment.author.slug, id: this.comment.author.id }
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
isAuthor(id) {
|
||||
return this.user.id === id
|
||||
},
|
||||
editCommentMenu(showMenu) {
|
||||
this.openEditCommentMenu = showMenu
|
||||
this.$emit('toggleNewCommentForm', !showMenu)
|
||||
},
|
||||
updateComment(comment) {
|
||||
this.$emit('updateComment', comment)
|
||||
},
|
||||
async deleteCommentCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
variables: { id: this.comment.id },
|
||||
})
|
||||
this.$toast.success(this.$t(`delete.comment.success`))
|
||||
this.$emit('deleteComment', DeleteComment)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.collapse-button {
|
||||
// TODO: move this to css resets
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
float: right;
|
||||
padding: 0 16px 16px 16px;
|
||||
color: $color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
.reply-button:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
border: 1px solid $color-primary;
|
||||
}
|
||||
100% {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.comment--target {
|
||||
animation: highlight 4s ease;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import Comment from './Comment.vue'
|
||||
import CommentCard from './CommentCard.vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -8,11 +8,17 @@ localVue.directive('scrollTo', jest.fn())
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
|
||||
describe('Comment.vue', () => {
|
||||
describe('CommentCard.vue', () => {
|
||||
let propsData, mocks, stubs, getters, wrapper, Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
propsData = {
|
||||
comment: {
|
||||
id: 'comment007',
|
||||
author: { id: 'some-user' },
|
||||
},
|
||||
postId: 'post42',
|
||||
}
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$toast: {
|
||||
@ -26,6 +32,7 @@ describe('Comment.vue', () => {
|
||||
truncate: a => a,
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$route: { hash: '' },
|
||||
$scrollTo: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
@ -55,7 +62,7 @@ describe('Comment.vue', () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return mount(Comment, {
|
||||
return mount(CommentCard, {
|
||||
store,
|
||||
propsData,
|
||||
mocks,
|
||||
@ -1,6 +1,6 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import Comment from './Comment'
|
||||
import CommentCard from './CommentCard'
|
||||
import helpers from '~/storybook/helpers'
|
||||
|
||||
helpers.init()
|
||||
@ -41,14 +41,14 @@ const comment = {
|
||||
__typename: 'Comment',
|
||||
}
|
||||
|
||||
storiesOf('Comment', module)
|
||||
storiesOf('CommentCard', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('Basic comment', () => ({
|
||||
components: { Comment },
|
||||
components: { CommentCard },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
comment,
|
||||
}),
|
||||
template: `<comment :key="comment.id" :comment="comment" />`,
|
||||
template: `<comment-card :key="comment.id" :comment="comment" />`,
|
||||
}))
|
||||
216
webapp/components/CommentCard/CommentCard.vue
Normal file
216
webapp/components/CommentCard/CommentCard.vue
Normal file
@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<base-card v-if="isUnavailable" class="comment-card">
|
||||
<p>
|
||||
<base-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
</p>
|
||||
</base-card>
|
||||
<base-card v-else :class="commentClass" :id="anchor">
|
||||
<header class="header">
|
||||
<user-teaser :user="comment.author" :date-time="comment.createdAt">
|
||||
<template v-if="wasEdited" #dateTime>
|
||||
<span>({{ $t('comment.edited') }})</span>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
v-show="!editingComment"
|
||||
placement="bottom-end"
|
||||
resource-type="comment"
|
||||
:resource="comment"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="user.id === comment.author.id"
|
||||
@editComment="editComment(true)"
|
||||
/>
|
||||
</client-only>
|
||||
</header>
|
||||
<comment-form
|
||||
v-if="editingComment"
|
||||
:update="true"
|
||||
:postId="postId"
|
||||
:comment="comment"
|
||||
@finishEditing="editComment(false)"
|
||||
@updateComment="updateComment"
|
||||
@collapse="isCollapsed = true"
|
||||
/>
|
||||
<template v-else>
|
||||
<content-viewer :content="commentContent" class="content" />
|
||||
<base-button v-if="hasLongContent" size="small" ghost @click="isCollapsed = !isCollapsed">
|
||||
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
|
||||
</base-button>
|
||||
</template>
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
size="small"
|
||||
v-scroll-to="'.editor'"
|
||||
@click="reply"
|
||||
/>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserTeaser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
CommentForm,
|
||||
},
|
||||
mixins: [scrollToAnchor],
|
||||
data() {
|
||||
const anchor = `commentId-${this.comment.id}`
|
||||
const isTarget = this.$route.hash === `#${anchor}`
|
||||
|
||||
return {
|
||||
anchor,
|
||||
isTarget,
|
||||
isCollapsed: !isTarget,
|
||||
editingComment: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
comment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
postId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
isModerator: 'auth/isModerator',
|
||||
}),
|
||||
hasLongContent() {
|
||||
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
|
||||
},
|
||||
isUnavailable() {
|
||||
return (this.comment.deleted || this.comment.disabled) && !this.isModerator
|
||||
},
|
||||
wasEdited() {
|
||||
return this.comment.createdAt !== this.comment.updatedAt
|
||||
},
|
||||
commentClass() {
|
||||
let commentClass = 'comment-card'
|
||||
|
||||
if (this.comment.deleted || this.comment.disabled) commentClass += ' disabled-content'
|
||||
if (this.isTarget) commentClass += ' --target'
|
||||
|
||||
return commentClass
|
||||
},
|
||||
commentContent() {
|
||||
if (this.hasLongContent && this.isCollapsed) {
|
||||
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
|
||||
}
|
||||
|
||||
return this.comment.content
|
||||
},
|
||||
menuModalsData() {
|
||||
return {
|
||||
delete: {
|
||||
titleIdent: 'delete.comment.title',
|
||||
messageIdent: 'delete.comment.message',
|
||||
messageParams: {
|
||||
name: this.$filters.truncate(this.comment.contentExcerpt, 30),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'delete.submit',
|
||||
callback: this.deleteCommentCallback,
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'delete.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
reply() {
|
||||
const message = { slug: this.comment.author.slug, id: this.comment.author.id }
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
editComment(editing) {
|
||||
this.editingComment = editing
|
||||
this.$emit('toggleNewCommentForm', !editing)
|
||||
},
|
||||
updateComment(comment) {
|
||||
this.$emit('updateComment', comment)
|
||||
},
|
||||
async deleteCommentCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
variables: { id: this.comment.id },
|
||||
})
|
||||
this.$toast.success(this.$t(`delete.comment.success`))
|
||||
this.$emit('deleteComment', DeleteComment)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.comment-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: $space-small;
|
||||
|
||||
&.--target {
|
||||
animation: highlight 4s ease;
|
||||
}
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
.reply-button:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
border: $border-size-base solid $color-primary;
|
||||
}
|
||||
100% {
|
||||
border: $border-size-base solid transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -153,10 +153,10 @@ describe('CommentForm.vue', () => {
|
||||
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('emits `showEditCommentMenu` event', async () => {
|
||||
it('emits `finishEditing` event', async () => {
|
||||
wrapper.vm.updateEditorContent('ok')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
|
||||
expect(wrapper.emitted('finishEditing')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@ -167,10 +167,10 @@ describe('CommentForm.vue', () => {
|
||||
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('emits `showEditCommentMenu` event', async () => {
|
||||
it('emits `finishEditing` event', async () => {
|
||||
wrapper.vm.updateEditorContent('ok')
|
||||
await wrapper.find('[data-test="cancel-button"]').trigger('submit')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
|
||||
expect(wrapper.emitted('finishEditing')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card>
|
||||
<!-- with client-only the content is not shown -->
|
||||
<base-card>
|
||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||
<div class="buttons">
|
||||
<base-button
|
||||
@ -17,7 +16,7 @@
|
||||
{{ $t('post.comment.submit') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
@ -72,7 +71,7 @@ export default {
|
||||
this.$refs.editor.clear()
|
||||
},
|
||||
closeEditWindow() {
|
||||
this.$emit('showEditCommentMenu', false)
|
||||
this.$emit('finishEditing')
|
||||
},
|
||||
handleCancel() {
|
||||
if (!this.update) {
|
||||
@ -146,10 +145,13 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
.comment-form {
|
||||
.editor {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: $space-small 0;
|
||||
|
||||
> .base-button {
|
||||
margin-left: $space-x-small;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import CommentList from './CommentList'
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
|
||||
@ -20,9 +20,14 @@ describe('CommentList.vue', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
post: {
|
||||
id: 1,
|
||||
id: 'post42',
|
||||
comments: [
|
||||
{ id: 'comment134', contentExcerpt: 'this is a comment', content: 'this is a comment' },
|
||||
{
|
||||
id: 'comment134',
|
||||
contentExcerpt: 'this is a comment',
|
||||
content: 'this is a comment',
|
||||
author: { id: 'some-user' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -41,6 +46,7 @@ describe('CommentList.vue', () => {
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$scrollTo: jest.fn(),
|
||||
$route: { hash: '' },
|
||||
$apollo: {
|
||||
queries: {
|
||||
Post: {
|
||||
@ -73,12 +79,6 @@ describe('CommentList.vue', () => {
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('$route.hash !== `#comments`', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '',
|
||||
}
|
||||
})
|
||||
|
||||
it('skips $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
@ -107,7 +107,7 @@ describe('CommentList.vue', () => {
|
||||
})
|
||||
|
||||
it('Comment emitted reply()', () => {
|
||||
wrapper.find(Comment).vm.$emit('reply', {
|
||||
wrapper.find(CommentCard).vm.$emit('reply', {
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
})
|
||||
|
||||
@ -4,15 +4,12 @@
|
||||
<counter-icon icon="comments" :count="postComments.length" />
|
||||
{{ $t('common.comment', null, 0) }}
|
||||
</h3>
|
||||
<ds-space margin-bottom="large" />
|
||||
<div id="comments" class="comments">
|
||||
<comment
|
||||
<div v-if="postComments" id="comments" class="comments">
|
||||
<comment-card
|
||||
v-for="comment in postComments"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
:routeHash="routeHash"
|
||||
class="comment-tag"
|
||||
:postId="post.id"
|
||||
@deleteComment="updateCommentList"
|
||||
@updateComment="updateCommentList"
|
||||
@toggleNewCommentForm="toggleNewCommentForm"
|
||||
@ -23,18 +20,20 @@
|
||||
</template>
|
||||
<script>
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
components: {
|
||||
CounterIcon,
|
||||
Comment,
|
||||
CommentCard,
|
||||
},
|
||||
props: {
|
||||
routeHash: { type: String, default: () => '' },
|
||||
post: { type: Object, default: () => {} },
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
postComments() {
|
||||
|
||||
@ -154,7 +154,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter(item => item.text() === 'comment.menu.edit')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[true]])
|
||||
expect(wrapper.emitted('editComment')).toBeTruthy()
|
||||
})
|
||||
it('delete the comment', () => {
|
||||
wrapper
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
size="small"
|
||||
circle
|
||||
ghost
|
||||
@click="toggleMenu"
|
||||
@click.prevent="toggleMenu()"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
@ -104,7 +104,7 @@ export default {
|
||||
routes.push({
|
||||
label: this.$t(`comment.menu.edit`),
|
||||
callback: () => {
|
||||
this.$emit('showEditCommentMenu', true)
|
||||
this.$emit('editComment')
|
||||
},
|
||||
icon: 'edit',
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import Vuex from 'vuex'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
|
||||
import TeaserImage from '~/components/TeaserImage/TeaserImage'
|
||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||
import MutationObserver from 'mutation-observer'
|
||||
|
||||
global.MutationObserver = MutationObserver
|
||||
@ -182,7 +182,7 @@ describe('ContributionForm.vue', () => {
|
||||
})
|
||||
|
||||
it('has no more than three categories', async () => {
|
||||
wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
|
||||
wrapper.vm.formData.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
|
||||
await Vue.nextTick()
|
||||
wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
@ -233,10 +233,13 @@ describe('ContributionForm.vue', () => {
|
||||
})
|
||||
|
||||
it('supports adding a teaser image', async () => {
|
||||
const spy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {})
|
||||
expectedParams.variables.imageUpload = imageUpload
|
||||
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
|
||||
wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload)
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
expect(spy).toHaveBeenCalledWith(imageUpload)
|
||||
spy.mockReset()
|
||||
})
|
||||
|
||||
it('content is valid with just a link', async () => {
|
||||
@ -320,20 +323,12 @@ describe('ContributionForm.vue', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('sets id equal to contribution id', () => {
|
||||
expect(wrapper.vm.id).toEqual(propsData.contribution.id)
|
||||
})
|
||||
|
||||
it('sets slug equal to contribution slug', () => {
|
||||
expect(wrapper.vm.slug).toEqual(propsData.contribution.slug)
|
||||
})
|
||||
|
||||
it('sets title equal to contribution title', () => {
|
||||
expect(wrapper.vm.form.title).toEqual(propsData.contribution.title)
|
||||
expect(wrapper.vm.formData.title).toEqual(propsData.contribution.title)
|
||||
})
|
||||
|
||||
it('sets content equal to contribution content', () => {
|
||||
expect(wrapper.vm.form.content).toEqual(propsData.contribution.content)
|
||||
expect(wrapper.vm.formData.content).toEqual(propsData.contribution.content)
|
||||
})
|
||||
|
||||
describe('valid update', () => {
|
||||
@ -362,6 +357,7 @@ describe('ContributionForm.vue', () => {
|
||||
image,
|
||||
imageUpload: null,
|
||||
imageAspectRatio: 1,
|
||||
imageBlurred: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -387,9 +383,10 @@ describe('ContributionForm.vue', () => {
|
||||
|
||||
it('supports deleting a teaser image', async () => {
|
||||
expectedParams.variables.image = null
|
||||
expectedParams.variables.imageAspectRatio = null
|
||||
propsData.contribution.image = '/uploads/someimage.png'
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('.contribution-form .delete-image').trigger('click')
|
||||
wrapper.find('[data-test="delete-button"]').trigger('click')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
})
|
||||
|
||||
@ -2,115 +2,75 @@
|
||||
<ds-form
|
||||
class="contribution-form"
|
||||
ref="contributionForm"
|
||||
v-model="form"
|
||||
v-model="formData"
|
||||
:schema="formSchema"
|
||||
@submit="submit"
|
||||
>
|
||||
<template slot-scope="{ errors }">
|
||||
<base-button
|
||||
v-if="showDeleteImageButton"
|
||||
class="delete-image"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click.prevent="deleteImage"
|
||||
/>
|
||||
<hc-teaser-image
|
||||
:contribution="contribution"
|
||||
:class="{ '--blur-image': form.blurImage }"
|
||||
@addTeaserImage="addTeaserImage"
|
||||
@addImageAspectRatio="addImageAspectRatio"
|
||||
@cropInProgress="cropInProgress"
|
||||
>
|
||||
<img
|
||||
v-if="contribution"
|
||||
class="contribution-image"
|
||||
:src="contribution.image | proxyApiUrl"
|
||||
/>
|
||||
</hc-teaser-image>
|
||||
|
||||
<ds-card>
|
||||
<div class="blur-toggle">
|
||||
<base-card>
|
||||
<template #heroImage>
|
||||
<img
|
||||
v-if="formData.image"
|
||||
:src="formData.image | proxyApiUrl"
|
||||
:class="['image', formData.imageBlurred && '--blur-image']"
|
||||
/>
|
||||
<image-uploader
|
||||
:hasImage="!!formData.image"
|
||||
:class="[formData.imageBlurred && '--blur-image']"
|
||||
@addHeroImage="addHeroImage"
|
||||
@addImageAspectRatio="addImageAspectRatio"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="formData.image" class="blur-toggle">
|
||||
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
|
||||
<input type="checkbox" id="blur-img" v-model="form.blurImage" />
|
||||
<p>
|
||||
<a
|
||||
href="https://support.human-connection.org/kb/faq.php?id=113"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
{{ $t('contribution.inappropriatePictureText') }}
|
||||
<ds-icon name="question-circle" />
|
||||
</a>
|
||||
</p>
|
||||
<input type="checkbox" id="blur-img" v-model="formData.imageBlurred" />
|
||||
<a
|
||||
href="https://support.human-connection.org/kb/faq.php?id=113"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
{{ $t('contribution.inappropriatePictureText') }}
|
||||
<base-icon name="question-circle" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ds-space />
|
||||
<client-only>
|
||||
<user-teaser :user="currentUser" />
|
||||
</client-only>
|
||||
<ds-space />
|
||||
<ds-input
|
||||
model="title"
|
||||
class="post-title"
|
||||
:placeholder="$t('contribution.title')"
|
||||
name="title"
|
||||
autofocus
|
||||
size="large"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.title" color="danger" size="base">
|
||||
{{ form.title.length }}/{{ formSchema.title.max }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
|
||||
</ds-text>
|
||||
<ds-chip size="base" :color="errors && errors.title && 'danger'">
|
||||
{{ formData.title.length }}/{{ formSchema.title.max }}
|
||||
<base-icon v-if="errors && errors.title" name="warning" />
|
||||
</ds-chip>
|
||||
<hc-editor
|
||||
:users="users"
|
||||
:value="form.content"
|
||||
:value="formData.content"
|
||||
:hashtags="hashtags"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.content" color="danger" size="base">
|
||||
{{ contentLength }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">
|
||||
{{ contentLength }}
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
<ds-space margin-bottom="small" />
|
||||
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
|
||||
{{ form.categoryIds.length }} / 3
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
|
||||
</ds-text>
|
||||
<ds-flex class="contribution-form-footer">
|
||||
<ds-flex-item :width="{ lg: '50%', md: '50%', sm: '100%' }" />
|
||||
<ds-flex-item>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-select
|
||||
model="language"
|
||||
:options="languageOptions"
|
||||
icon="globe"
|
||||
:placeholder="$t('contribution.languageSelectText')"
|
||||
:label="$t('contribution.languageSelectLabel')"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
|
||||
<ds-space />
|
||||
<div slot="footer" style="text-align: right">
|
||||
<ds-chip size="base" :color="errors && errors.content && 'danger'">
|
||||
{{ contentLength }}
|
||||
<base-icon v-if="errors && errors.content" name="warning" />
|
||||
</ds-chip>
|
||||
<categories-select model="categoryIds" :existingCategoryIds="formData.categoryIds" />
|
||||
<ds-chip size="base" :color="errors && errors.categoryIds && 'danger'">
|
||||
{{ formData.categoryIds.length }} / 3
|
||||
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
||||
</ds-chip>
|
||||
<ds-select
|
||||
model="language"
|
||||
icon="globe"
|
||||
class="select-field"
|
||||
:options="languageOptions"
|
||||
:placeholder="$t('contribution.languageSelectText')"
|
||||
:label="$t('contribution.languageSelectLabel')"
|
||||
/>
|
||||
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||
<base-icon name="warning" />
|
||||
</ds-chip>
|
||||
<div class="buttons">
|
||||
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
@ -118,8 +78,7 @@
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<ds-space margin-bottom="large" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
@ -131,127 +90,95 @@ import { mapGetters } from 'vuex'
|
||||
import HcEditor from '~/components/Editor/Editor'
|
||||
import locales from '~/locales'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HcEditor,
|
||||
HcCategoriesSelect,
|
||||
HcTeaserImage,
|
||||
UserTeaser,
|
||||
CategoriesSelect,
|
||||
ImageUploader,
|
||||
},
|
||||
props: {
|
||||
contribution: { type: Object, default: () => {} },
|
||||
contribution: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
imageAspectRatio,
|
||||
imageBlurred,
|
||||
language,
|
||||
categories,
|
||||
} = this.contribution
|
||||
|
||||
const languageOptions = orderBy(locales, 'name').map(locale => {
|
||||
return { label: locale.name, value: locale.code }
|
||||
})
|
||||
|
||||
const formDefaults = {
|
||||
title: '',
|
||||
content: '',
|
||||
teaserImage: null,
|
||||
imageAspectRatio: null,
|
||||
image: null,
|
||||
language: null,
|
||||
categoryIds: [],
|
||||
blurImage: false,
|
||||
}
|
||||
|
||||
let id = null
|
||||
let slug = null
|
||||
const form = { ...formDefaults }
|
||||
if (this.contribution && this.contribution.id) {
|
||||
id = this.contribution.id
|
||||
slug = this.contribution.slug
|
||||
form.title = this.contribution.title
|
||||
form.content = this.contribution.content
|
||||
form.image = this.contribution.image
|
||||
form.language =
|
||||
this.contribution && this.contribution.language
|
||||
? languageOptions.find(o => this.contribution.language === o.value)
|
||||
: null
|
||||
form.categoryIds = this.categoryIds(this.contribution.categories)
|
||||
form.imageAspectRatio = this.contribution.imageAspectRatio
|
||||
form.blurImage = this.contribution.imageBlurred
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
formData: {
|
||||
title: title || '',
|
||||
content: content || '',
|
||||
image: image || null,
|
||||
imageAspectRatio: imageAspectRatio || null,
|
||||
imageBlurred: imageBlurred || false,
|
||||
language: languageOptions.find(option => option.value === language) || null,
|
||||
categoryIds: categories ? categories.map(category => category.id) : [],
|
||||
},
|
||||
formSchema: {
|
||||
title: { required: true, min: 3, max: 100 },
|
||||
content: { required: true },
|
||||
categoryIds: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
validator: (rule, value) => {
|
||||
const errors = []
|
||||
if (!(value && value.length >= 1 && value.length <= 3)) {
|
||||
errors.push(new Error(this.$t('common.validations.categories')))
|
||||
validator: (_, value = []) => {
|
||||
if (value.length === 0 || value.length > 3) {
|
||||
return [new Error(this.$t('common.validations.categories'))]
|
||||
}
|
||||
return errors
|
||||
return []
|
||||
},
|
||||
},
|
||||
language: { required: true },
|
||||
blurImage: { required: false },
|
||||
imageBlurred: { required: false },
|
||||
},
|
||||
languageOptions,
|
||||
id,
|
||||
slug,
|
||||
loading: false,
|
||||
users: [],
|
||||
contentMin: 3,
|
||||
hashtags: [],
|
||||
elem: null,
|
||||
isCropInProgress: null,
|
||||
imageUpload: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
contentLength() {
|
||||
return this.$filters.removeHtml(this.form.content).length
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
showDeleteImageButton() {
|
||||
return this.contribution && this.contribution.image && !this.isCropInProgress
|
||||
contentLength() {
|
||||
return this.$filters.removeHtml(this.formData.content).length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const {
|
||||
language: { value: language },
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
teaserImage,
|
||||
imageAspectRatio,
|
||||
categoryIds,
|
||||
blurImage,
|
||||
} = this.form
|
||||
this.loading = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
mutation: this.contribution.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
variables: {
|
||||
id: this.id,
|
||||
title,
|
||||
content,
|
||||
categoryIds,
|
||||
language,
|
||||
image,
|
||||
imageUpload: teaserImage,
|
||||
imageBlurred: blurImage,
|
||||
imageAspectRatio,
|
||||
...this.formData,
|
||||
id: this.contribution.id || null,
|
||||
language: this.formData.language.value,
|
||||
image: this.imageUpload ? null : this.formData.image,
|
||||
imageUpload: this.imageUpload,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.loading = false
|
||||
this.$toast.success(this.$t('contribution.success'))
|
||||
const result = data[this.id ? 'UpdatePost' : 'CreatePost']
|
||||
const result = data[this.contribution.id ? 'UpdatePost' : 'CreatePost']
|
||||
|
||||
this.$router.push({
|
||||
name: 'post-id-slug',
|
||||
@ -266,22 +193,19 @@ export default {
|
||||
updateEditorContent(value) {
|
||||
this.$refs.contributionForm.update('content', value)
|
||||
},
|
||||
addTeaserImage(file) {
|
||||
this.form.teaserImage = file
|
||||
addHeroImage(file) {
|
||||
this.formData.image = null
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
this.formData.image = target.result
|
||||
}
|
||||
this.imageUpload = file
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
},
|
||||
addImageAspectRatio(aspectRatio) {
|
||||
this.form.imageAspectRatio = aspectRatio
|
||||
},
|
||||
categoryIds(categories) {
|
||||
return categories.map(c => c.id)
|
||||
},
|
||||
deleteImage() {
|
||||
this.contribution.image = null
|
||||
this.form.image = null
|
||||
this.form.teaserImage = null
|
||||
},
|
||||
cropInProgress(boolean) {
|
||||
this.isCropInProgress = boolean
|
||||
this.formData.imageAspectRatio = aspectRatio
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
@ -319,41 +243,48 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.contribution-form {
|
||||
.ds-card-image.--blur-image img {
|
||||
filter: blur(32px);
|
||||
.contribution-form > .base-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .hero-image {
|
||||
position: relative;
|
||||
|
||||
> .image {
|
||||
max-height: $size-image-max-height;
|
||||
}
|
||||
}
|
||||
|
||||
.image.--blur-image {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
> .ds-form-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .ds-chip {
|
||||
align-self: flex-end;
|
||||
margin: $space-xx-small 0 $space-base;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
> .select-field {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
align-self: flex-end;
|
||||
margin-top: $space-base;
|
||||
}
|
||||
|
||||
.blur-toggle {
|
||||
text-align: right;
|
||||
margin-bottom: $space-base;
|
||||
|
||||
> .link {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-chip {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-top: $space-x-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
|
||||
input {
|
||||
border: 0;
|
||||
font-size: $font-size-x-large;
|
||||
font-weight: bold;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete-image {
|
||||
right: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
float: right;
|
||||
top: $space-large;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -88,7 +88,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
describe('calls the delete user mutation', () => {
|
||||
beforeEach(() => {
|
||||
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||
enableDeletionInput = wrapper.find('.ds-input')
|
||||
enableDeletionInput.setValue(deleteAccountName)
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
})
|
||||
@ -107,7 +107,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's posts if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||
enableContributionDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -122,7 +122,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's comments if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||
enableCommentDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -137,10 +137,10 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's posts and comments if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||
enableContributionDeletionCheckbox.trigger('click')
|
||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||
enableCommentDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -166,7 +166,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
describe('error handling', () => {
|
||||
it('shows an error toaster when the mutation rejects', async () => {
|
||||
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||
enableDeletionInput = wrapper.find('.ds-input')
|
||||
enableDeletionInput.setValue(deleteAccountName)
|
||||
await Vue.nextTick()
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
|
||||
@ -1,80 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-card hover>
|
||||
<ds-space />
|
||||
<ds-container>
|
||||
<ds-flex>
|
||||
<ds-flex-item :width="{ base: '22%', sm: '12%', md: '12%', lg: '8%' }">
|
||||
<base-icon name="warning" class="delete-warning-icon" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '78%', sm: '88%', md: '88%', lg: '92%' }">
|
||||
<ds-heading>{{ $t('settings.deleteUserAccount.name') }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-space />
|
||||
<ds-heading tag="h4">
|
||||
{{ $t('settings.deleteUserAccount.accountDescription') }}
|
||||
</ds-heading>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
<ds-space />
|
||||
<ds-container>
|
||||
<transition name="slide-up">
|
||||
<div v-if="deleteEnabled">
|
||||
<label v-if="currentUser.contributionsCount" class="checkbox-container">
|
||||
<input type="checkbox" v-model="deleteContributions" />
|
||||
<span class="checkmark"></span>
|
||||
{{
|
||||
$t('settings.deleteUserAccount.contributionsCount', {
|
||||
count: currentUser.contributionsCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<ds-space margin-bottom="small" />
|
||||
<label v-if="currentUser.commentedCount" class="checkbox-container">
|
||||
<input type="checkbox" v-model="deleteComments" />
|
||||
<span class="checkmark"></span>
|
||||
{{
|
||||
$t('settings.deleteUserAccount.commentedCount', {
|
||||
count: currentUser.commentedCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-section id="delete-user-account-warning">
|
||||
<div v-html="$t('settings.deleteUserAccount.accountWarning')"></div>
|
||||
</ds-section>
|
||||
</div>
|
||||
</transition>
|
||||
</ds-container>
|
||||
<template slot="footer" class="delete-data-footer">
|
||||
<ds-container>
|
||||
<div
|
||||
class="delete-input-label"
|
||||
v-html="$t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name })"
|
||||
></div>
|
||||
<ds-space margin-bottom="xx-small" />
|
||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'large' }">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1.75 }">
|
||||
<ds-input v-model="enableDeletionValue" class="enable-deletion-input" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
|
||||
<base-button
|
||||
icon="trash"
|
||||
danger
|
||||
filled
|
||||
:disabled="!deleteEnabled"
|
||||
data-test="delete-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
</template>
|
||||
</ds-card>
|
||||
</div>
|
||||
<base-card class="delete-data">
|
||||
<h2 class="title">
|
||||
<base-icon name="warning" />
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</h2>
|
||||
<label>
|
||||
{{ $t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name }) }}
|
||||
</label>
|
||||
<ds-input v-model="enableDeletionValue" />
|
||||
<p class="notice">{{ $t('settings.deleteUserAccount.accountDescription') }}</p>
|
||||
<label v-if="currentUser.contributionsCount" class="checkbox">
|
||||
<input type="checkbox" v-model="deleteContributions" />
|
||||
{{
|
||||
$t('settings.deleteUserAccount.contributionsCount', {
|
||||
count: currentUser.contributionsCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<label v-if="currentUser.commentedCount" class="checkbox">
|
||||
<input type="checkbox" v-model="deleteComments" />
|
||||
{{
|
||||
$t('settings.deleteUserAccount.commentedCount', {
|
||||
count: currentUser.commentedCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<section v-if="deleteEnabled" class="warning">
|
||||
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
||||
</section>
|
||||
<base-button
|
||||
icon="trash"
|
||||
danger
|
||||
filled
|
||||
:disabled="!deleteEnabled"
|
||||
data-test="delete-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import gql from 'graphql-tag'
|
||||
@ -131,96 +97,47 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.delete-warning-icon {
|
||||
color: $color-danger;
|
||||
font-size: $font-size-xxx-large;
|
||||
}
|
||||
.delete-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.checkbox-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 35px;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-large;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
> .title > .base-icon {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.checkbox-container input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
> .ds-form-item {
|
||||
align-self: flex-start;
|
||||
margin-top: $space-xxx-small;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border: 2px solid $background-color-inverse-softer;
|
||||
background-color: $background-color-base;
|
||||
border-radius: $border-radius-x-large;
|
||||
}
|
||||
> .notice {
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.checkbox-container:hover input ~ .checkmark {
|
||||
background-color: $background-color-softest;
|
||||
}
|
||||
> .checkbox {
|
||||
margin-left: $space-base;
|
||||
margin-bottom: $space-x-small;
|
||||
|
||||
.checkbox-container input:checked ~ .checkmark {
|
||||
background-color: $background-color-danger-active;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
> .warning {
|
||||
padding: $space-large;
|
||||
margin-bottom: $space-small;
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.checkbox-container input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
color: $color-danger;
|
||||
background-color: $color-danger-inverse;
|
||||
border-left: 4px solid $color-danger;
|
||||
}
|
||||
|
||||
.checkbox-container .checkmark:after {
|
||||
left: 6px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid $background-color-base;
|
||||
border-width: 0 $border-size-large $border-size-large 0;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.enable-deletion-input input:focus {
|
||||
border-color: $border-color-danger;
|
||||
}
|
||||
|
||||
.delete-input-label {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
b.is-danger {
|
||||
color: $text-color-danger;
|
||||
}
|
||||
|
||||
.delete-data-footer {
|
||||
border-top: $border-size-base solid $border-color-softest;
|
||||
background-color: $background-color-danger-inverse;
|
||||
}
|
||||
|
||||
#delete-user-account-warning {
|
||||
background-color: $background-color-danger-inverse;
|
||||
border-left: $border-size-x-large solid $background-color-danger-active;
|
||||
color: $text-color-danger;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
border-radius: $border-radius-x-large;
|
||||
> .base-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -40,9 +40,9 @@ storiesOf('Editor', module)
|
||||
return {
|
||||
components: { ctx },
|
||||
template: `
|
||||
<ds-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||
<base-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||
<ctx />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
`,
|
||||
}
|
||||
})
|
||||
|
||||
@ -89,7 +89,7 @@ export default {
|
||||
}
|
||||
|
||||
&.hint {
|
||||
opacity: 0.7;
|
||||
opacity: $opacity-soft;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ describe('FilterMenu.vue', () => {
|
||||
mocks = { $t: () => {} }
|
||||
})
|
||||
|
||||
describe('given a user', () => {
|
||||
describe('given a hashtag', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
hashtag: null,
|
||||
hashtag: 'Frieden',
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,19 +27,14 @@ describe('FilterMenu.vue', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('does not render a card if there are no hashtags', () => {
|
||||
expect(wrapper.is('.ds-card')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders a card if there are hashtags', () => {
|
||||
propsData.hashtag = 'Frieden'
|
||||
it('renders a card', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.is('.ds-card')).toBe(true)
|
||||
expect(wrapper.is('.base-card')).toBe(true)
|
||||
})
|
||||
|
||||
describe('click "clear-search-button" button', () => {
|
||||
describe('click clear search button', () => {
|
||||
it('emits clearSearch', () => {
|
||||
wrapper.find('[name="clear-search-button"]').trigger('click')
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,32 +1,22 @@
|
||||
<template>
|
||||
<ds-card class="filter-menu-card">
|
||||
<ds-flex class="filter-menu-content">
|
||||
<ds-flex-item>
|
||||
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<div class="filter-menu-buttons">
|
||||
<base-button
|
||||
name="clear-search-button"
|
||||
icon="close"
|
||||
circle
|
||||
@click="clearSearch"
|
||||
v-tooltip="{
|
||||
content: this.$t('filter-menu.clearSearch'),
|
||||
placement: 'left',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
<base-card class="filter-menu">
|
||||
<h2>{{ $t('filter-menu.hashtag-search', { hashtag }) }}</h2>
|
||||
<base-button
|
||||
icon="close"
|
||||
circle
|
||||
:title="this.$t('filter-menu.clearSearch')"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
hashtag: { type: String, default: null },
|
||||
hashtag: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearSearch() {
|
||||
@ -37,21 +27,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.filter-menu-card {
|
||||
background-color: $background-color-soft;
|
||||
}
|
||||
|
||||
.filter-menu-content {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-menu-title {
|
||||
.filter-menu.base-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-menu-buttons {
|
||||
float: right;
|
||||
padding: $space-x-small $space-base;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TeaserImage from './TeaserImage.vue'
|
||||
import ImageUploader from './ImageUploader.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('TeaserImage.vue', () => {
|
||||
describe('ImageUploader.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
|
||||
@ -17,7 +17,7 @@ describe('TeaserImage.vue', () => {
|
||||
})
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(TeaserImage, { mocks, localVue })
|
||||
return mount(ImageUploader, { mocks, localVue })
|
||||
}
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
@ -28,21 +28,10 @@ describe('TeaserImage.vue', () => {
|
||||
const message = 'File upload failed'
|
||||
const fileError = { status: 'error' }
|
||||
|
||||
it('defaults to error false', () => {
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
|
||||
it('shows an error toaster when verror is called', () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
wrapper.vm.onDropzoneError(fileError, message)
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
|
||||
})
|
||||
|
||||
it('changes error status from false to true to false', () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
expect(wrapper.vm.error).toEqual(true)
|
||||
jest.runAllTimers()
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
200
webapp/components/ImageUploader/ImageUploader.vue
Normal file
200
webapp/components/ImageUploader/ImageUploader.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="image-uploader">
|
||||
<vue-dropzone
|
||||
v-show="!showCropper"
|
||||
id="postdropzone"
|
||||
:options="dropzoneOptions"
|
||||
:use-custom-slot="true"
|
||||
@vdropzone-error="onDropzoneError"
|
||||
@vdropzone-file-added="initCropper"
|
||||
>
|
||||
<loading-spinner v-if="isLoadingImage" />
|
||||
<base-icon v-else name="image" />
|
||||
<base-button
|
||||
v-if="hasImage"
|
||||
icon="trash"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
data-test="delete-button"
|
||||
:title="$t('actions.delete')"
|
||||
@click.stop="deleteImage"
|
||||
/>
|
||||
</vue-dropzone>
|
||||
<div v-show="showCropper" class="crop-overlay">
|
||||
<img id="cropping-image" />
|
||||
<base-button class="crop-confirm" filled @click="cropImage">
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click="closeCropper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueDropzone from 'nuxt-dropzone'
|
||||
import Cropper from 'cropperjs'
|
||||
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
VueDropzone,
|
||||
},
|
||||
props: {
|
||||
hasImage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
url: () => '',
|
||||
maxFilesize: 5.0,
|
||||
previewTemplate: '<span class="no-preview" />',
|
||||
},
|
||||
cropper: null,
|
||||
file: null,
|
||||
showCropper: false,
|
||||
isLoadingImage: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDropzoneError(file, message) {
|
||||
this.$toast.error(file.status, message)
|
||||
},
|
||||
initCropper(file) {
|
||||
this.showCropper = true
|
||||
this.file = file
|
||||
|
||||
const imageElement = document.querySelector('#cropping-image')
|
||||
imageElement.src = URL.createObjectURL(file)
|
||||
this.cropper = new Cropper(imageElement, { zoomable: false, autoCropArea: 0.9 })
|
||||
},
|
||||
cropImage() {
|
||||
this.isLoadingImage = true
|
||||
|
||||
const onCropComplete = (aspectRatio, imageFile) => {
|
||||
this.$emit('addImageAspectRatio', aspectRatio)
|
||||
this.$emit('addHeroImage', imageFile)
|
||||
this.$nextTick((this.isLoadingImage = false))
|
||||
this.closeCropper()
|
||||
}
|
||||
|
||||
if (this.file.type === 'image/jpeg') {
|
||||
const canvas = this.cropper.getCroppedCanvas()
|
||||
canvas.toBlob(blob => {
|
||||
const imageAspectRatio = canvas.width / canvas.height
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
onCropComplete(imageAspectRatio, croppedImageFile)
|
||||
}, 'image/jpeg')
|
||||
} else {
|
||||
// TODO: use cropped file instead of original file
|
||||
const imageAspectRatio = this.file.width / this.file.height || 1.0
|
||||
onCropComplete(imageAspectRatio, this.file)
|
||||
}
|
||||
},
|
||||
closeCropper() {
|
||||
this.showCropper = false
|
||||
this.cropper.destroy()
|
||||
},
|
||||
deleteImage() {
|
||||
this.$emit('addHeroImage', null)
|
||||
this.$emit('addImageAspectRatio', null)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.image-uploader {
|
||||
position: relative;
|
||||
min-height: $size-image-uploader-min-height;
|
||||
cursor: pointer;
|
||||
|
||||
.image + & {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
background-color: $color-neutral-85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .crop-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: $size-image-cropper-min-height;
|
||||
max-height: $size-image-cropper-max-height;
|
||||
font-size: $font-size-base;
|
||||
|
||||
> .img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
> .crop-confirm {
|
||||
position: absolute;
|
||||
left: $space-x-small;
|
||||
top: $space-x-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
|
||||
> .crop-cancel {
|
||||
position: absolute;
|
||||
right: $space-x-small;
|
||||
top: $space-x-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
}
|
||||
|
||||
.dz-message {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: $z-index-surface;
|
||||
|
||||
&:hover {
|
||||
> .base-icon {
|
||||
opacity: $opacity-base;
|
||||
}
|
||||
}
|
||||
|
||||
> .base-icon {
|
||||
position: absolute;
|
||||
padding: $space-small;
|
||||
border-radius: 100%;
|
||||
border: $border-size-base dashed $color-neutral-20;
|
||||
background-color: $color-neutral-95;
|
||||
font-size: $size-icon-large;
|
||||
opacity: $opacity-soft;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
right: $space-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,10 +1,12 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
|
||||
import LocaleSwitch from './LocaleSwitch.vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
|
||||
describe('LocaleSwitch.vue', () => {
|
||||
let wrapper, mocks, computed, deutschLanguageItem, getters
|
||||
|
||||
|
||||
@ -1,35 +1,37 @@
|
||||
<template>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<a
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu-popover"
|
||||
:matcher="matcher"
|
||||
:routes="routes"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
<client-only>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<a
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</dropdown>
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu-popover"
|
||||
:matcher="matcher"
|
||||
:routes="routes"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</dropdown>
|
||||
</client-only>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,71 +1,54 @@
|
||||
<template>
|
||||
<ds-container width="medium">
|
||||
<ds-space margin="small">
|
||||
<blockquote>
|
||||
<p>{{ $t('quotes.african.quote') }}</p>
|
||||
<b>- {{ $t('quotes.african.author') }}</b>
|
||||
</blockquote>
|
||||
</ds-space>
|
||||
<ds-card class="login-card">
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<client-only>
|
||||
<locale-switch class="login-locale-switch" offset="5" />
|
||||
</client-only>
|
||||
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
|
||||
<img
|
||||
class="login-image"
|
||||
alt="Human Connection"
|
||||
src="/img/sign-up/humanconnection.svg"
|
||||
/>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<ds-space margin="small">
|
||||
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfoHint')" target="_blank">
|
||||
{{ $t('login.moreInfo') }}
|
||||
</a>
|
||||
</ds-space>
|
||||
<ds-space margin="small">
|
||||
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
|
||||
</ds-space>
|
||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||
<ds-input
|
||||
v-model="form.email"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.email')"
|
||||
type="email"
|
||||
name="email"
|
||||
icon="envelope"
|
||||
/>
|
||||
<ds-input
|
||||
v-model="form.password"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.password')"
|
||||
icon="lock"
|
||||
icon-right="question-circle"
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
<ds-space margin-bottom="large">
|
||||
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
|
||||
</ds-space>
|
||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||
{{ $t('login.login') }}
|
||||
</base-button>
|
||||
<ds-space margin-top="large" margin-bottom="x-small">
|
||||
{{ $t('login.no-account') }}
|
||||
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
|
||||
</ds-space>
|
||||
</form>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</ds-container>
|
||||
<section class="login-form">
|
||||
<blockquote>
|
||||
<p>{{ $t('quotes.african.quote') }}</p>
|
||||
<b>- {{ $t('quotes.african.author') }}</b>
|
||||
</blockquote>
|
||||
<base-card>
|
||||
<template #imageColumn>
|
||||
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfo')" target="_blank">
|
||||
<img class="image" alt="Human Connection" src="/img/sign-up/humanconnection.svg" />
|
||||
</a>
|
||||
</template>
|
||||
<h2 class="title">{{ $t('login.login') }}</h2>
|
||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||
<ds-input
|
||||
v-model="form.email"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.email')"
|
||||
type="email"
|
||||
name="email"
|
||||
icon="envelope"
|
||||
/>
|
||||
<ds-input
|
||||
v-model="form.password"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.password')"
|
||||
icon="lock"
|
||||
icon-right="question-circle"
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
<nuxt-link to="/password-reset/request">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</nuxt-link>
|
||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||
{{ $t('login.login') }}
|
||||
</base-button>
|
||||
<p>
|
||||
{{ $t('login.no-account') }}
|
||||
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
|
||||
</p>
|
||||
</form>
|
||||
<template #topMenu>
|
||||
<locale-switch offset="5" />
|
||||
</template>
|
||||
</base-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch.vue'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -100,21 +83,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login-image {
|
||||
width: 90%;
|
||||
max-width: 200px;
|
||||
}
|
||||
.login-card {
|
||||
position: relative;
|
||||
.login-form {
|
||||
width: 80vw;
|
||||
max-width: 620px;
|
||||
margin: auto;
|
||||
|
||||
.base-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: $space-large;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
.login-locale-switch {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
left: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -63,7 +63,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.commented_on_post',
|
||||
)
|
||||
})
|
||||
@ -79,9 +79,9 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -90,8 +90,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -113,7 +113,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.mentioned_in_post',
|
||||
)
|
||||
})
|
||||
@ -125,9 +125,9 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
|
||||
})
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -136,8 +136,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -163,7 +163,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.mentioned_in_comment',
|
||||
)
|
||||
})
|
||||
@ -182,9 +182,9 @@ describe('Notification', () => {
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -193,8 +193,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,37 +1,23 @@
|
||||
<template>
|
||||
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
|
||||
<article :class="{ '--read': notification.read, notification: true }">
|
||||
<client-only>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<user-teaser :user="from.author" :date-time="from.createdAt" />
|
||||
</ds-space>
|
||||
<ds-text class="reason-text-for-test" color="soft">
|
||||
{{ $t(`notifications.reason.${notification.reason}`) }}
|
||||
</ds-text>
|
||||
<user-teaser :user="from.author" :date-time="from.createdAt" />
|
||||
</client-only>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>
|
||||
<nuxt-link
|
||||
class="notification-mention-post"
|
||||
class="link"
|
||||
:to="{ name: 'post-id-slug', params, ...hashParam }"
|
||||
@click.native="$emit('read')"
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<ds-card
|
||||
:header="from.title || from.post.title"
|
||||
hover
|
||||
space="x-small"
|
||||
class="notifications-card"
|
||||
>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<div>
|
||||
<span v-if="isComment" class="comment-notification-header">
|
||||
{{ $t(`notifications.comment`) }}:
|
||||
</span>
|
||||
{{ from.contentExcerpt | removeHtml }}
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
<base-card wideContent>
|
||||
<h2 class="title">{{ from.title || from.post.title }}</h2>
|
||||
<p>
|
||||
<strong v-if="isComment" class="comment">{{ $t(`notifications.comment`) }}:</strong>
|
||||
{{ from.contentExcerpt | removeHtml }}
|
||||
</p>
|
||||
</base-card>
|
||||
</nuxt-link>
|
||||
</ds-space>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -70,14 +56,36 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notification.read {
|
||||
opacity: $opacity-soft;
|
||||
}
|
||||
.notifications-card {
|
||||
min-width: 500px;
|
||||
}
|
||||
.comment-notification-header {
|
||||
font-weight: 700;
|
||||
margin-right: 0.1rem;
|
||||
.notification {
|
||||
margin-bottom: $space-base;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: $space-x-small;
|
||||
}
|
||||
|
||||
&.--read {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
|
||||
> .description {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
> .link {
|
||||
display: block;
|
||||
color: $text-color-base;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.user-teaser {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
.comment {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -73,7 +73,7 @@ describe('NotificationList.vue', () => {
|
||||
|
||||
describe('click on a notification', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.notification-mention-post').trigger('click')
|
||||
wrapper.find('.notification > .link').trigger('click')
|
||||
})
|
||||
|
||||
it("emits 'markAsRead' with the id of the notification source", () => {
|
||||
|
||||
@ -3,7 +3,7 @@ import { withA11y } from '@storybook/addon-a11y'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import { post } from '~/components/PostCard/PostCard.story.js'
|
||||
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
|
||||
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||
|
||||
helpers.init()
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<ds-card
|
||||
:lang="post.language"
|
||||
:image="post.image | proxyApiUrl"
|
||||
:class="{
|
||||
'post-card': true,
|
||||
'disabled-content': post.disabled,
|
||||
'--pinned': isPinned,
|
||||
'--blur-image': post.imageBlurred,
|
||||
}"
|
||||
>
|
||||
<!-- Post Link Target -->
|
||||
<nuxt-link
|
||||
class="post-link"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Username, Image & Date of Post -->
|
||||
<div class="user-wrapper">
|
||||
<client-only>
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt" />
|
||||
</client-only>
|
||||
<hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" />
|
||||
<hc-ribbon v-else :text="$t('post.name')" />
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Title -->
|
||||
<ds-heading tag="h3" class="hyphenate-text post-title">{{ post.title }}</ds-heading>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Content Excerpt -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<div class="hc-editor-content hyphenate-text" v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- Footer o the Post -->
|
||||
<template slot="footer">
|
||||
<div style="display: inline-block; opacity: .5;">
|
||||
<!-- Categories -->
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.name.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
:icon="category.icon"
|
||||
/>
|
||||
</div>
|
||||
<client-only>
|
||||
<div style="display: inline-block; float: right">
|
||||
<!-- Shouts Count -->
|
||||
<span :style="{ opacity: post.shoutedCount ? 1 : 0.5 }">
|
||||
<base-icon name="bullhorn" />
|
||||
<small>{{ post.shoutedCount }}</small>
|
||||
</span>
|
||||
|
||||
<!-- Comments Count -->
|
||||
<span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
|
||||
<base-icon name="comments" />
|
||||
<small>{{ post.commentsCount }}</small>
|
||||
</span>
|
||||
<!-- Menu -->
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</div>
|
||||
</client-only>
|
||||
</template>
|
||||
</ds-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcRibbon from '~/components/Ribbon'
|
||||
// import { randomBytes } from 'crypto'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
|
||||
export default {
|
||||
name: 'HcPostCard',
|
||||
components: {
|
||||
UserTeaser,
|
||||
HcCategory,
|
||||
HcRibbon,
|
||||
ContentMenu,
|
||||
},
|
||||
props: {
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const width = this.$el.offsetWidth
|
||||
const height = Math.min(width / this.post.imageAspectRatio, 2000)
|
||||
const imageElement = this.$el.querySelector('.ds-card-image')
|
||||
if (imageElement) {
|
||||
imageElement.style.height = `${height}px`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
excerpt() {
|
||||
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||
},
|
||||
isAuthor() {
|
||||
const { author } = this.post
|
||||
if (!author) return false
|
||||
return this.user.id === this.post.author.id
|
||||
},
|
||||
menuModalsData() {
|
||||
return postMenuModalsData(
|
||||
// "this.post" may not always be defined at the beginning …
|
||||
this.post ? this.$filters.truncate(this.post.title, 30) : '',
|
||||
this.deletePostCallback,
|
||||
)
|
||||
},
|
||||
isPinned() {
|
||||
return this.post && this.post.pinned
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeletePost },
|
||||
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||
this.$toast.success(this.$t('delete.contribution.success'))
|
||||
this.$emit('removePostFromList', DeletePost)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
pinPost(post) {
|
||||
this.$emit('pinPost', post)
|
||||
},
|
||||
unpinPost(post) {
|
||||
this.$emit('unpinPost', post)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.post-card {
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.--pinned {
|
||||
border: 1px solid $color-warning;
|
||||
}
|
||||
|
||||
&.--blur-image > .ds-card-image img {
|
||||
filter: blur(22px);
|
||||
}
|
||||
|
||||
> .ds-card-image img {
|
||||
width: 100%;
|
||||
max-height: 2000px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .ds-card-content {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* workaround to avoid jumping layout when footer is rendered */
|
||||
> .ds-card-footer {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-top: $space-large;
|
||||
}
|
||||
|
||||
/* workaround to avoid jumping layout when user-teaser is rendered */
|
||||
.user-wrapper {
|
||||
height: 36px;
|
||||
position: relative;
|
||||
z-index: $z-index-post-card-link;
|
||||
}
|
||||
|
||||
.content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-card-link;
|
||||
display: inline-block;
|
||||
margin-left: $space-xx-small;
|
||||
margin-right: -$space-x-small;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
margin: 15px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-indent: -999999px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,14 +2,14 @@ import { config, shallowMount, mount, RouterLinkStub } from '@vue/test-utils'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import PostCard from './PostCard.vue'
|
||||
import PostTeaser from './PostTeaser.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||
|
||||
describe('PostCard', () => {
|
||||
describe('PostTeaser', () => {
|
||||
let store
|
||||
let stubs
|
||||
let mocks
|
||||
@ -22,11 +22,13 @@ describe('PostCard', () => {
|
||||
propsData = {
|
||||
post: {
|
||||
id: 'p23',
|
||||
disabled: false,
|
||||
shoutedCount: 0,
|
||||
commentsCount: 0,
|
||||
name: 'It is a post',
|
||||
author: {
|
||||
id: 'u1',
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
stubs = {
|
||||
@ -55,7 +57,7 @@ describe('PostCard', () => {
|
||||
describe('shallowMount', () => {
|
||||
Wrapper = () => {
|
||||
store = new Vuex.Store({ getters })
|
||||
return shallowMount(PostCard, {
|
||||
return shallowMount(PostTeaser, {
|
||||
store,
|
||||
propsData,
|
||||
mocks,
|
||||
@ -63,6 +65,13 @@ describe('PostCard', () => {
|
||||
})
|
||||
}
|
||||
|
||||
it('has no validation errors', () => {
|
||||
const spy = jest.spyOn(global.console, 'error')
|
||||
Wrapper()
|
||||
expect(spy).not.toBeCalled()
|
||||
spy.mockReset()
|
||||
})
|
||||
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('test Post callbacks', () => {
|
||||
@ -99,7 +108,7 @@ describe('PostCard', () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return mount(PostCard, {
|
||||
return mount(PostTeaser, {
|
||||
stubs,
|
||||
mocks,
|
||||
propsData,
|
||||
@ -111,6 +120,7 @@ describe('PostCard', () => {
|
||||
describe('given a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData.post = {
|
||||
...propsData.post,
|
||||
title: "It's a title",
|
||||
}
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import HcPostCard from './PostCard.vue'
|
||||
import PostTeaser from './PostTeaser.vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
|
||||
helpers.init()
|
||||
@ -44,24 +44,24 @@ export const post = {
|
||||
__typename: 'Post',
|
||||
}
|
||||
|
||||
storiesOf('Post Card', module)
|
||||
storiesOf('PostTeaser', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('without image', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post,
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
.add('with image', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post: {
|
||||
@ -70,27 +70,23 @@ storiesOf('Post Card', module)
|
||||
},
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
.add('pinned by admin', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post: {
|
||||
...post,
|
||||
pinnedBy: {
|
||||
id: '4711',
|
||||
name: 'Ad Min',
|
||||
role: 'admin',
|
||||
},
|
||||
pinned: true,
|
||||
},
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
207
webapp/components/PostTeaser/PostTeaser.vue
Normal file
207
webapp/components/PostTeaser/PostTeaser.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<nuxt-link
|
||||
class="post-teaser"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
<base-card
|
||||
:lang="post.language"
|
||||
:class="{
|
||||
'disabled-content': post.disabled,
|
||||
'--blur-image': post.imageBlurred,
|
||||
}"
|
||||
:highlight="isPinned"
|
||||
>
|
||||
<template v-if="post.image" #heroImage>
|
||||
<img :src="post.image | proxyApiUrl" class="image" />
|
||||
</template>
|
||||
<client-only>
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt" />
|
||||
</client-only>
|
||||
<h2 class="title hyphenate-text">{{ post.title }}</h2>
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="content hyphenate-text" v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<footer class="footer">
|
||||
<div class="categories">
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.name.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
:icon="category.icon"
|
||||
/>
|
||||
</div>
|
||||
<counter-icon
|
||||
icon="bullhorn"
|
||||
:count="post.shoutedCount"
|
||||
:title="$t('contribution.amount-shouts', { amount: post.shoutedCount })"
|
||||
/>
|
||||
<counter-icon
|
||||
icon="comments"
|
||||
:count="post.commentsCount"
|
||||
:title="$t('contribution.amount-comments', { amount: post.commentsCount })"
|
||||
/>
|
||||
<client-only>
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</client-only>
|
||||
</footer>
|
||||
</base-card>
|
||||
<hc-ribbon
|
||||
:class="{ '--pinned': isPinned }"
|
||||
:text="isPinned ? $t('post.pinned') : $t('post.name')"
|
||||
/>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcRibbon from '~/components/Ribbon'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
|
||||
export default {
|
||||
name: 'PostTeaser',
|
||||
components: {
|
||||
UserTeaser,
|
||||
HcCategory,
|
||||
HcRibbon,
|
||||
ContentMenu,
|
||||
CounterIcon,
|
||||
},
|
||||
props: {
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const width = this.$el.offsetWidth
|
||||
const height = Math.min(width / this.post.imageAspectRatio, 2000)
|
||||
const imageElement = this.$el.querySelector('.hero-image')
|
||||
if (imageElement) {
|
||||
imageElement.style.height = `${height}px`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
excerpt() {
|
||||
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||
},
|
||||
isAuthor() {
|
||||
const { author } = this.post
|
||||
if (!author) return false
|
||||
return this.user.id === this.post.author.id
|
||||
},
|
||||
menuModalsData() {
|
||||
return postMenuModalsData(
|
||||
// "this.post" may not always be defined at the beginning …
|
||||
this.post ? this.$filters.truncate(this.post.title, 30) : '',
|
||||
this.deletePostCallback,
|
||||
)
|
||||
},
|
||||
isPinned() {
|
||||
return this.post && this.post.pinned
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeletePost },
|
||||
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||
this.$toast.success(this.$t('delete.contribution.success'))
|
||||
this.$emit('removePostFromList', DeletePost)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
pinPost(post) {
|
||||
this.$emit('pinPost', post)
|
||||
},
|
||||
unpinPost(post) {
|
||||
this.$emit('unpinPost', post)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.post-teaser,
|
||||
.post-teaser:hover,
|
||||
.post-teaser:active {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
color: $text-color-base;
|
||||
|
||||
> .ribbon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-teaser > .base-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&.--blur-image > .hero-image > .image {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
> .content {
|
||||
flex-grow: 1;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
> .footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
> .categories {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .counter-icon {
|
||||
display: block;
|
||||
margin-right: $space-small;
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
|
||||
> .content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-teaser-link;
|
||||
}
|
||||
|
||||
.ds-tag {
|
||||
margin: 0;
|
||||
margin-right: $space-xx-small;
|
||||
}
|
||||
}
|
||||
|
||||
.user-teaser {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -45,13 +45,13 @@ export default {
|
||||
border-style: solid;
|
||||
border-color: $background-color-secondary transparent transparent $background-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon--pinned {
|
||||
background-color: $color-warning-active;
|
||||
&.--pinned {
|
||||
background-color: $color-warning;
|
||||
|
||||
&::before {
|
||||
border-color: $color-warning transparent transparent $color-warning;
|
||||
&::before {
|
||||
border-color: $color-warning transparent transparent $color-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,257 +0,0 @@
|
||||
<template>
|
||||
<vue-dropzone
|
||||
:options="dropzoneOptions"
|
||||
ref="el"
|
||||
id="postdropzone"
|
||||
class="ds-card-image"
|
||||
:use-custom-slot="true"
|
||||
@vdropzone-error="verror"
|
||||
@vdropzone-thumbnail="transformImage"
|
||||
>
|
||||
<div class="crop-overlay" ref="cropperOverlay" v-show="showCropper">
|
||||
<base-button @click="cropImage" class="crop-confirm" filled>
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click="cancelCrop"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'hc-attachments-upload-area-post': true,
|
||||
'hc-attachments-upload-area-update-post': contribution,
|
||||
}"
|
||||
>
|
||||
<slot></slot>
|
||||
<div
|
||||
:class="{
|
||||
'hc-drag-marker-post': true,
|
||||
'hc-drag-marker-update-post': contribution,
|
||||
}"
|
||||
>
|
||||
<base-icon name="image" />
|
||||
</div>
|
||||
</div>
|
||||
</vue-dropzone>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
vueDropzone,
|
||||
},
|
||||
props: {
|
||||
contribution: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
url: () => '',
|
||||
maxFilesize: 5.0,
|
||||
previewTemplate: this.template(),
|
||||
},
|
||||
image: null,
|
||||
file: null,
|
||||
editor: null,
|
||||
cropper: null,
|
||||
thumbnailElement: null,
|
||||
oldImage: null,
|
||||
error: false,
|
||||
showCropper: false,
|
||||
imageAspectRatio: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
template() {
|
||||
return `<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-image">
|
||||
<div data-dz-thumbnail-bg></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
verror(file, message) {
|
||||
this.error = true
|
||||
this.$toast.error(file.status, message)
|
||||
setTimeout(() => {
|
||||
this.error = false
|
||||
}, 2000)
|
||||
},
|
||||
transformImage(file) {
|
||||
this.file = file
|
||||
this.$emit('cropInProgress', true)
|
||||
this.showCropper = true
|
||||
this.initEditor()
|
||||
this.initCropper()
|
||||
},
|
||||
initEditor() {
|
||||
this.editor = this.$refs.cropperOverlay
|
||||
this.clearImages()
|
||||
this.thumbnailElement.appendChild(this.editor)
|
||||
},
|
||||
clearImages() {
|
||||
this.thumbnailElement = document.querySelectorAll('#postdropzone')[0]
|
||||
const thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
|
||||
if (thumbnailPreview) thumbnailPreview.remove()
|
||||
const contributionImage = document.querySelectorAll('.contribution-image')[0]
|
||||
this.oldImage = contributionImage
|
||||
if (contributionImage) contributionImage.remove()
|
||||
},
|
||||
deleteImage() {
|
||||
this.clearImages()
|
||||
},
|
||||
initCropper() {
|
||||
this.image = new Image()
|
||||
this.image.src = URL.createObjectURL(this.file)
|
||||
this.editor.appendChild(this.image)
|
||||
this.cropper = new Cropper(this.image, { zoomable: false, autoCropArea: 0.9 })
|
||||
},
|
||||
cropImage() {
|
||||
this.showCropper = false
|
||||
if (this.file.type === 'image/jpeg') {
|
||||
this.uploadJpeg()
|
||||
} else {
|
||||
this.uploadOtherImageType()
|
||||
}
|
||||
},
|
||||
uploadOtherImageType() {
|
||||
this.imageAspectRatio = this.file.width / this.file.height || 1.0
|
||||
this.image = new Image()
|
||||
this.image.src = this.file.dataURL
|
||||
this.setupPreview()
|
||||
this.emitImageData(this.file)
|
||||
},
|
||||
uploadJpeg() {
|
||||
const canvas = this.cropper.getCroppedCanvas()
|
||||
canvas.toBlob(blob => {
|
||||
this.imageAspectRatio = canvas.width / canvas.height
|
||||
this.image = new Image()
|
||||
this.image.src = canvas.toDataURL()
|
||||
this.setupPreview()
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
this.emitImageData(croppedImageFile)
|
||||
}, 'image/jpeg')
|
||||
},
|
||||
setupPreview() {
|
||||
this.image.classList.add('thumbnail-preview')
|
||||
this.thumbnailElement.appendChild(this.image)
|
||||
},
|
||||
cancelCrop() {
|
||||
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
|
||||
this.showCropper = false
|
||||
this.$emit('cropInProgress', false)
|
||||
},
|
||||
emitImageData(imageFile) {
|
||||
this.$emit('addTeaserImage', imageFile)
|
||||
this.$emit('addImageAspectRatio', this.imageAspectRatio)
|
||||
this.$emit('cropInProgress', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#postdropzone {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
background-color: $background-color-softest;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-post {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-update-post img {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-update-post:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hc-drag-marker-post {
|
||||
position: absolute;
|
||||
width: 122px;
|
||||
height: 122px;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 180px 5px;
|
||||
color: hsl(0, 0%, 25%);
|
||||
transition: all 0.2s ease-out;
|
||||
font-size: 60px;
|
||||
background-color: $background-color-softest;
|
||||
opacity: 0.65;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
border: 20px solid $text-color-base;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border-radius: 100%;
|
||||
border: $border-size-base dashed $text-color-base;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-post:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hc-drag-marker-update-post {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.contribution-form-footer {
|
||||
border-top: $border-size-base solid $border-color-softest;
|
||||
}
|
||||
|
||||
.crop-overlay {
|
||||
max-height: 2000px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.crop-confirm {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
.crop-cancel {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -80,7 +80,7 @@ storiesOf('UserTeaser', module)
|
||||
}),
|
||||
template: `
|
||||
<user-teaser :user="user" :date-time="new Date()">
|
||||
<template v-slot:dateTime>
|
||||
<template #dateTime>
|
||||
- HEY! I'm edited
|
||||
</template>
|
||||
</user>
|
||||
|
||||
78
webapp/components/_new/generic/BaseCard/BaseCard.story.js
Normal file
78
webapp/components/_new/generic/BaseCard/BaseCard.story.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import BaseCard from './BaseCard.vue'
|
||||
|
||||
storiesOf('Generic/BaseCard', module)
|
||||
.addDecorator(helpers.layout)
|
||||
|
||||
.add('default', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: hero image', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 400px;">
|
||||
<template #heroImage>
|
||||
<img class="image" src="https://unsplash.com/photos/R4y_E5ZQDPg/download" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: image column', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 600px;">
|
||||
<template #imageColumn>
|
||||
<img class="image" src="/img/sign-up/humanconnection.svg" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: topMenu', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 600px;">
|
||||
<template #imageColumn>
|
||||
<img class="image" src="/img/sign-up/humanconnection.svg" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
<template #topMenu>
|
||||
<base-button size="small">Menu</base-button>
|
||||
</template>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with highlight prop', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card highlight style="width: 400px;">
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with wideContent prop', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card wideContent style="width: 400px;">
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
132
webapp/components/_new/generic/BaseCard/BaseCard.vue
Normal file
132
webapp/components/_new/generic/BaseCard/BaseCard.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<article :class="classNames">
|
||||
<template v-if="$slots.imageColumn">
|
||||
<aside class="image-column">
|
||||
<slot name="imageColumn" />
|
||||
</aside>
|
||||
<section class="content-column">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$slots.heroImage">
|
||||
<section class="hero-image">
|
||||
<slot name="heroImage" />
|
||||
</section>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<slot v-else />
|
||||
<aside v-if="$slots.topMenu" class="top-menu">
|
||||
<slot name="topMenu" />
|
||||
</aside>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
highlight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wideContent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
let classNames = 'base-card'
|
||||
|
||||
if (this.$slots.imageColumn) classNames += ' --columns'
|
||||
if (this.highlight) classNames += ' --highlight'
|
||||
if (this.wideContent) classNames += ' --wide-content'
|
||||
|
||||
return classNames
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.base-card {
|
||||
position: relative;
|
||||
padding: $space-base;
|
||||
border-radius: $border-radius-x-large;
|
||||
overflow: hidden;
|
||||
background-color: $color-neutral-100;
|
||||
box-shadow: $box-shadow-base;
|
||||
|
||||
&.--columns {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.--highlight {
|
||||
border: $border-size-base solid $color-warning;
|
||||
}
|
||||
|
||||
&.--wide-content {
|
||||
padding: $space-small;
|
||||
|
||||
> .hero-image {
|
||||
width: calc(100% + (2 * #{$space-small}));
|
||||
margin: -$space-small;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
> .title,
|
||||
> .content-column > .title {
|
||||
font-size: $font-size-large;
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
> .hero-image {
|
||||
width: calc(100% + (2 * #{$space-base}));
|
||||
max-height: $size-image-max-height;
|
||||
margin: -$space-base;
|
||||
margin-bottom: $space-base;
|
||||
overflow: hidden;
|
||||
|
||||
> .image {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
> .image-column {
|
||||
flex-basis: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: $space-base;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
> .content-column {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
> .top-menu {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
left: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 565px) {
|
||||
.base-card.--columns {
|
||||
flex-direction: column;
|
||||
|
||||
> .image-column {
|
||||
padding-right: 0;
|
||||
margin-bottom: $space-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,7 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import { post } from '~/components/PostCard/PostCard.story.js'
|
||||
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
|
||||
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import ReportList from './ReportList'
|
||||
@ -183,11 +183,11 @@ storiesOf('ReportList', module)
|
||||
openModal: action('openModal'),
|
||||
filter: action('filter'),
|
||||
},
|
||||
template: `<ds-card>
|
||||
template: `<base-card>
|
||||
<div class="reports-header">
|
||||
<h3 class="title">Reports</h3>
|
||||
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||
</div>
|
||||
<reports-table :reports="reports" @confirm="openModal" />
|
||||
</ds-card>`,
|
||||
</base-card>`,
|
||||
}))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-card>
|
||||
<base-card>
|
||||
<div class="reports-header">
|
||||
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
|
||||
<client-only>
|
||||
@ -8,8 +8,9 @@
|
||||
</div>
|
||||
<reports-table :reports="reports" @confirm="openModal" />
|
||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||
@ -166,7 +167,7 @@ export default {
|
||||
.reports-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: $space-small 0;
|
||||
margin-bottom: $space-small;
|
||||
|
||||
> .title {
|
||||
margin: 0;
|
||||
|
||||
@ -10,9 +10,7 @@
|
||||
</a>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="20%" style="flex-grow:0;">
|
||||
<client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
||||
</client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
|
||||
@ -50,9 +50,7 @@
|
||||
}"
|
||||
style="flex-basis: auto;"
|
||||
>
|
||||
<client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
</client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
<template v-if="isLoggedIn">
|
||||
<client-only>
|
||||
<notification-menu placement="top" />
|
||||
|
||||
@ -217,6 +217,8 @@
|
||||
}
|
||||
},
|
||||
"contribution": {
|
||||
"amount-comments": "{amount} comments",
|
||||
"amount-shouts": "{amount} recommendations",
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
||||
},
|
||||
@ -635,12 +637,12 @@
|
||||
"success": "Deine Daten wurden erfolgreich aktualisiert!"
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Sei Dir bewusst, dass Deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn Du sie trotzdem löschen möchtest, musst Du sie unten markieren.",
|
||||
"accountWarning": "Dein Konto, Deine Beiträge oder Kommentare kannst Du nach dem Löschen <b>WEDER VERWALTEN NOCH WIEDERHERSTELLEN!</b>",
|
||||
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
||||
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!",
|
||||
"commentedCount": "Meine {count} Kommentare löschen",
|
||||
"contributionsCount": "Meine {count} Beiträge löschen",
|
||||
"name": "Benutzerkonto löschen",
|
||||
"pleaseConfirm": "<b class='is-danger'>Zerstörerische Aktion!</b> Gib <b>{confirm}</b> ein, um zu bestätigen.",
|
||||
"pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.",
|
||||
"success": "Konto erfolgreich gelöscht!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -217,6 +217,8 @@
|
||||
}
|
||||
},
|
||||
"contribution": {
|
||||
"amount-comments": "{amount} comments",
|
||||
"amount-shouts": "{amount} recommendations",
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
||||
},
|
||||
@ -636,11 +638,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Be aware that your Posts and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
|
||||
"accountWarning": "You CAN'T MANAGE and CAN'T RECOVER your Account, Posts, or Comments after deleting your account!",
|
||||
"commentedCount": "Delete my {count} comments",
|
||||
"contributionsCount": "Delete my {count} posts",
|
||||
"name": "Delete user account",
|
||||
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm",
|
||||
"pleaseConfirm": "Destructive action! Type “{confirm}” to confirm.",
|
||||
"success": "Account successfully deleted!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -609,11 +609,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Tenga en cuenta que su contribución y sus comentarios son importantes para nuestra comunidad. Si aún decide borrarlos, debe marcarlos a continuación.",
|
||||
"accountWarning": "¡<b> NO PUEDE GESTIONAR </b> y <b> NO PUEDE RECUPERAR </b> su cuenta, contribuciones o comentarios después de eliminar su cuenta!",
|
||||
"accountWarning": "¡NO PUEDE GESTIONAR y NO PUEDE RECUPERAR su cuenta, contribuciones o comentarios después de eliminar su cuenta!",
|
||||
"commentedCount": "Eliminar mis {count} comentarios",
|
||||
"contributionsCount": "Eliminar mis {count} contribuciones",
|
||||
"name": "Eliminar cuenta de usuario",
|
||||
"pleaseConfirm": "<b class='is-danger'> ¡Acción destructiva! </b> Escriba <b> {confirm} </b> para confirmar",
|
||||
"pleaseConfirm": "¡Acción destructiva! Escriba “{confirm}” para confirmar.",
|
||||
"success": "¡Cuenta eliminada con éxito!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -159,7 +159,7 @@
|
||||
"project": "Projet ::: Projets",
|
||||
"reportContent": "Signaler",
|
||||
"shout": "Partage ::: Partages",
|
||||
"tag": "Tag ::: Tags",
|
||||
"tag": "Tag ::: Tags",
|
||||
"takeAction": "Passer à l'action",
|
||||
"user": "Utilisateur ::: Utilisateurs",
|
||||
"validations": {
|
||||
@ -551,11 +551,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Sachez que vos postes et commentaires sont importants pour notre communauté. Si vous voulez quand même les supprimer, vous devez les marquer ci-dessous.",
|
||||
"accountWarning": "Vous <b>NE POUVEZ PAS GÉRER</b> et <b>NE POUVEZ PAS RECOUVRIR</b> votre compte, vos messages ou vos commentaires après avoir supprimé votre compte !",
|
||||
"accountWarning": "Vous NE POUVEZ PAS GÉRER et NE POUVEZ PAS RECOUVRIR votre compte, vos messages ou vos commentaires après avoir supprimé votre compte!",
|
||||
"commentedCount": "Supprimer mes {count} commentaires",
|
||||
"contributionsCount": "Supprimer mes {count} postes",
|
||||
"name": "Supprimer un compte utilisateur",
|
||||
"pleaseConfirm": "<b class='is-danger'> Action destructive! </b> Saisissez <b> {confirm} </b> pour confirmer",
|
||||
"pleaseConfirm": "Action destructive! Saisissez “{confirm}” pour confirmer.",
|
||||
"success": "Compte supprimer avec succès!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -551,11 +551,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Essere consapevoli che i tuoi post e commenti sono importanti per la nostra comunità. Se cancelli il tuo account utente, tutto scomparirà per sempre - e sarebbe un vero peccato!",
|
||||
"accountWarning": "Attenzione!Tu <b>Non puoi gestire</b> e <b>Non puoi recuperare il tuo account, i tuoi messaggi o commenti dopo aver cancellato il tuo account!",
|
||||
"accountWarning": "Attenzione! Tu NON PUOI GESTIRE e NON PUOI RECUPERARE il tuo account, i tuoi messaggi o commenti dopo aver cancellato il tuo account!",
|
||||
"commentedCount": "Cancella i miei {count} commenti",
|
||||
"contributionsCount": "Cancellare i miei {count} messaggi",
|
||||
"name": "Cancellare l'account utente",
|
||||
"pleaseConfirm": "<b class='is-danger'>Azione distruttiva! </b> Digita <b>{conferma}</b> per confermare",
|
||||
"pleaseConfirm": "Azione distruttiva! Digita “{confirm}” per confermare.",
|
||||
"success": "Account eliminato con successo!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -295,11 +295,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountWarning": "Po usunięcie Twojego konta, nie możesz <b>ZARZĄDZAĆ</b> ani <b>ODZYSKAĆ</b> danych, wpisów oraz komentarzy!",
|
||||
"accountWarning": "Po usunięcie Twojego konta, nie możesz ZARZĄDZAĆ ani ODZYSKAĆ danych, wpisów oraz komentarzy!",
|
||||
"commentedCount": "Usuń {count} moich komentarzy",
|
||||
"contributionsCount": "Usuń {count} moich postów",
|
||||
"name": "Usuń dane",
|
||||
"pleaseConfirm": "<b class='is-danger'>Uwaga, niebezpieczeństwo!</b> Wpisz <b>{confirm}</b>, aby potwierdzić",
|
||||
"pleaseConfirm": "Uwaga, niebezpieczeństwo! Wpisz „{confirm}”, aby potwierdzić.",
|
||||
"success": "Konto zostało usunięte"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -551,11 +551,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Esteja ciente de que o suas Publicações e Comentários são importantes para a nossa comunidade. Se você ainda optar por excluí-los, você tem que marcá-los abaixo.",
|
||||
"accountWarning": "Você <b>NÃO PODE GERENCIAR</b> e <b>NÃO PODE RECUPERAR</b> sua conta, Publicações, ou Comentários após excluir sua conta!",
|
||||
"accountWarning": "Você NÃO PODE GERENCIAR e NÃO PODE RECUPERAR sua conta, Publicações, ou Comentários após excluir sua conta!",
|
||||
"commentedCount": "Deletar meus {count} comentários",
|
||||
"contributionsCount": "Deletar minhas {count} publicações",
|
||||
"name": "Deletar dados",
|
||||
"pleaseConfirm": "<b class='is-danger'>Ação destrutiva!</b> Digitar <b>{confirm}</b> para confirmar",
|
||||
"pleaseConfirm": "Ação destrutiva! Digitar “{confirm}” para confirmar.",
|
||||
"success": "Conta eliminada com sucesso!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -609,11 +609,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Обратите внимание, что ваши посты и комментарии важны для сообщества. Если вы все равно хотите их удалить, то вы должны отметить соответствующие опции ниже.",
|
||||
"accountWarning": "Вы <b>НЕ СМОЖЕТЕ</b> восстановить свой аккаунт, посты или комментарии после удаления.",
|
||||
"accountWarning": "Вы НЕ СМОЖЕТЕ восстановить свой аккаунт, посты или комментарии после удаления.",
|
||||
"commentedCount": "Удалить мои комментарии: {count}",
|
||||
"contributionsCount": "Удалить мои посты: {count}",
|
||||
"name": "Удалить данные",
|
||||
"pleaseConfirm": "<b class='is-danger'>Разрушительное действие!</b> Введите <b>{confirm}</b> для подтверждения.",
|
||||
"pleaseConfirm": "Разрушительное действие! Введите „{confirm}“ для подтверждения.",
|
||||
"success": "Аккаунт успешно удален!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<transition name="fade" appear>
|
||||
<ds-container width="medium">
|
||||
<ds-card>
|
||||
<base-card>
|
||||
<ds-space>
|
||||
<client-only>
|
||||
<locale-switch class="login-locale-switch" offset="5" />
|
||||
</client-only>
|
||||
<locale-switch class="login-locale-switch" offset="5" />
|
||||
</ds-space>
|
||||
<ds-flex>
|
||||
<ds-flex-item :width="{ base: '100%', sm: 1, md: 1 }">
|
||||
@ -31,7 +29,7 @@
|
||||
</ds-flex-item>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-container>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.categories.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.categories.name') }}</h2>
|
||||
<ds-table :data="Category" :fields="fields" condensed>
|
||||
<template slot="icon" slot-scope="scope">
|
||||
<base-icon :name="scope.row.icon" />
|
||||
</template>
|
||||
</ds-table>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.donations.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.donations.name') }}</h2>
|
||||
<ds-form v-model="formData" @submit="submit">
|
||||
<ds-input model="goal" :label="$t('admin.donations.goal')" placeholder="15000" icon="money" />
|
||||
<ds-input
|
||||
@ -12,7 +13,7 @@
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</ds-form>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.hashtags.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.hashtags.name') }}</h2>
|
||||
<ds-table :data="Tag" :fields="fields" condensed>
|
||||
<template slot="index" slot-scope="scope">{{ scope.index + 1 }}.</template>
|
||||
<template slot="id" slot-scope="scope">
|
||||
@ -8,7 +9,7 @@
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</ds-table>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-card>
|
||||
<base-card>
|
||||
<ApolloQuery :query="Statistics">
|
||||
<template v-slot="{ result: { loading, error, data } }">
|
||||
<template v-if="loading">
|
||||
@ -123,7 +123,7 @@
|
||||
</template>
|
||||
</template>
|
||||
</ApolloQuery>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<!-- This registration form will be visible to the public as soon as the registration is open to the public. -->
|
||||
<ds-section>
|
||||
<ds-space>
|
||||
<ds-heading size="h3">{{ $t('admin.invites.title') }}</ds-heading>
|
||||
<ds-text>{{ $t('admin.invites.description') }}</ds-text>
|
||||
</ds-space>
|
||||
<ds-card class="signup">
|
||||
<base-card>
|
||||
<signup :invitation="true" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-section>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.notifications.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.notifications.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.organizations.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.organizations.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.pages.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.pages.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('admin.settings.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.settings.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-card :header="$t('admin.users.name')">
|
||||
<ds-form v-model="form" @submit="submit">
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item width="90%">
|
||||
<ds-input
|
||||
model="query"
|
||||
:placeholder="$t('admin.users.form.placeholder')"
|
||||
icon="search"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="30px">
|
||||
<base-button filled circle type="submit" icon="search" :loading="$apollo.loading" />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-form>
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
<ds-card v-if="User && User.length">
|
||||
<div class="admin-users">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('admin.users.name') }}</h2>
|
||||
<ds-form v-model="form" @submit="submit">
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item width="90%">
|
||||
<ds-input
|
||||
model="query"
|
||||
:placeholder="$t('admin.users.form.placeholder')"
|
||||
icon="search"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="30px">
|
||||
<base-button filled circle type="submit" icon="search" :loading="$apollo.loading" />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-form>
|
||||
</base-card>
|
||||
<base-card v-if="User && User.length">
|
||||
<ds-table :data="User" :fields="fields" condensed>
|
||||
<template slot="index" slot-scope="scope">{{ scope.row.index + 1 }}.</template>
|
||||
<template slot="name" slot-scope="scope">
|
||||
@ -51,10 +50,10 @@
|
||||
</template>
|
||||
</ds-table>
|
||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @next="next" @back="back" />
|
||||
</ds-card>
|
||||
<ds-card v-else>
|
||||
</base-card>
|
||||
<base-card v-else>
|
||||
<ds-placeholder>{{ $t('admin.users.empty') }}</ds-placeholder>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -178,3 +177,9 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-users > .base-card:first-child {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<masonry-grid>
|
||||
<ds-grid-item v-show="hashtag" :row-span="2" column-span="fullWidth">
|
||||
<ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth">
|
||||
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" />
|
||||
</ds-grid-item>
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth" class="top-info-bar">
|
||||
@ -26,7 +26,7 @@
|
||||
:key="post.id"
|
||||
:imageAspectRatio="post.imageAspectRatio"
|
||||
>
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
@removePostFromList="deletePost"
|
||||
@pinPost="pinPost"
|
||||
@ -67,7 +67,7 @@
|
||||
// import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
|
||||
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
import HcPostCard from '~/components/PostCard/PostCard.vue'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
components: {
|
||||
// DonationInfo,
|
||||
FilterMenu,
|
||||
HcPostCard,
|
||||
PostTeaser,
|
||||
HcEmpty,
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
@ -219,11 +219,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ds-card-image img {
|
||||
max-height: 2000px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.masonry-grid {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-card space="small">
|
||||
<base-card>
|
||||
<ds-flex class="notifications-page-flex">
|
||||
<ds-flex-item :width="{ lg: '85%' }">
|
||||
<ds-heading tag="h3">{{ $t('notifications.title') }}</ds-heading>
|
||||
@ -16,7 +16,7 @@
|
||||
:notifications="notifications"
|
||||
/>
|
||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,20 +1,14 @@
|
||||
<template>
|
||||
<ds-container width="small">
|
||||
<ds-card>
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '40%' }">
|
||||
<client-only>
|
||||
<locale-switch offset="5" />
|
||||
</client-only>
|
||||
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
|
||||
<img alt="Human Connection" src="/icon.png" />
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '60%' }" centered>
|
||||
<nuxt-child />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
<ds-container width="small" class="password-reset">
|
||||
<base-card>
|
||||
<template #imageColumn>
|
||||
<img alt="Human Connection" src="/icon.png" class="image" />
|
||||
</template>
|
||||
<nuxt-child />
|
||||
<template #topMenu>
|
||||
<locale-switch offset="5" />
|
||||
</template>
|
||||
</base-card>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
@ -33,10 +27,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
img {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,42 +1,46 @@
|
||||
<template>
|
||||
<transition name="fade" appear>
|
||||
<ds-card
|
||||
:lang="post.language"
|
||||
<base-card
|
||||
v-if="post && ready"
|
||||
:image="post.image | proxyApiUrl"
|
||||
:lang="post.language"
|
||||
:class="{
|
||||
'post-page': true,
|
||||
'disabled-content': post.disabled,
|
||||
'--blur-image': blurred,
|
||||
}"
|
||||
>
|
||||
<aside v-show="post.imageBlurred" class="blur-toggle">
|
||||
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
|
||||
<base-button
|
||||
:icon="blurred ? 'eye' : 'eye-slash'"
|
||||
filled
|
||||
circle
|
||||
@click="blurred = !blurred"
|
||||
/>
|
||||
</aside>
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt">
|
||||
<template v-slot:dateTime>
|
||||
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
placement="bottom-end"
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</client-only>
|
||||
<template #heroImage v-if="post.image">
|
||||
<img :src="post.image | proxyApiUrl" class="image" />
|
||||
<aside v-show="post.imageBlurred" class="blur-toggle">
|
||||
<img v-show="blurred" :src="post.image | proxyApiUrl" class="preview" />
|
||||
<base-button
|
||||
:icon="blurred ? 'eye' : 'eye-slash'"
|
||||
filled
|
||||
circle
|
||||
@click="blurred = !blurred"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
<section class="menu">
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt">
|
||||
<template #dateTime>
|
||||
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
placement="bottom-end"
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</client-only>
|
||||
</section>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading>
|
||||
<h2 class="title hyphenate-text">{{ post.title }}</h2>
|
||||
<ds-space margin-bottom="small" />
|
||||
<content-viewer class="content hyphenate-text" :content="post.content" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
@ -83,13 +87,8 @@
|
||||
</ds-flex>
|
||||
</ds-space>
|
||||
<!-- Comments -->
|
||||
<ds-section slot="footer">
|
||||
<comment-list
|
||||
:post="post"
|
||||
:routeHash="$route.hash"
|
||||
@toggleNewCommentForm="toggleNewCommentForm"
|
||||
@reply="reply"
|
||||
/>
|
||||
<ds-section>
|
||||
<comment-list :post="post" @toggleNewCommentForm="toggleNewCommentForm" @reply="reply" />
|
||||
<ds-space margin-bottom="large" />
|
||||
<comment-form
|
||||
v-if="showNewCommentForm && !isBlocked"
|
||||
@ -104,7 +103,7 @@
|
||||
<a href="https://support.human-connection.org/kb/" target="_blank">FAQ</a>
|
||||
</ds-placeholder>
|
||||
</ds-section>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@ -246,18 +245,23 @@ export default {
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.post-page {
|
||||
&.--blur-image > .ds-card-image img {
|
||||
filter: blur(22px);
|
||||
> .hero-image {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ds-card-content {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
> .menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.--blur-image > .hero-image > .image {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
.blur-toggle {
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
@ -272,40 +276,13 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.content-menu {
|
||||
float: right;
|
||||
margin-right: -$space-x-small;
|
||||
margin-top: -$space-large;
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin-top: $space-small;
|
||||
|
||||
.comment {
|
||||
margin-top: $space-small;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
min-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-card-image {
|
||||
img {
|
||||
max-height: 2000px;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-card-footer {
|
||||
padding: 0;
|
||||
|
||||
.ds-section {
|
||||
padding: $space-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-card>
|
||||
<h2 style="margin-bottom: .2em;">{{ $t('post.moreInfo.title') }}</h2>
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('post.moreInfo.title') }}</h2>
|
||||
<p>{{ $t('post.moreInfo.description') }}</p>
|
||||
<ds-space />
|
||||
<h3>{{ $t('post.moreInfo.titleOfCategoriesSection') }}</h3>
|
||||
@ -24,7 +24,7 @@
|
||||
:key="relatedPost.id"
|
||||
:imageAspectRatio="relatedPost.imageAspectRatio"
|
||||
>
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="relatedPost"
|
||||
:width="{ base: '100%', lg: 1 }"
|
||||
@removePostFromList="removePostFromList"
|
||||
@ -33,12 +33,12 @@
|
||||
</masonry-grid>
|
||||
<hc-empty v-else margin="large" icon="file" message="No related Posts" />
|
||||
</ds-section>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
import HcPostCard from '~/components/PostCard/PostCard.vue'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcHashtag from '~/components/Hashtag/Hashtag'
|
||||
import { relatedContributions } from '~/graphql/PostQuery'
|
||||
@ -51,7 +51,7 @@ export default {
|
||||
mode: 'out-in',
|
||||
},
|
||||
components: {
|
||||
HcPostCard,
|
||||
PostTeaser,
|
||||
HcCategory,
|
||||
HcHashtag,
|
||||
HcEmpty,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card header="Werde aktiv!">
|
||||
<base-card>
|
||||
<h2 class="title">Werde aktiv!</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-card v-if="user && user.image">
|
||||
<p>PROFILE IMAGE</p>
|
||||
</ds-card>
|
||||
<ds-space />
|
||||
<ds-flex v-if="user" :width="{ base: '100%' }" gutter="base">
|
||||
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
|
||||
<ds-card
|
||||
<base-card
|
||||
:class="{ 'disabled-content': user.disabled }"
|
||||
style="position: relative; height: auto;"
|
||||
style="position: relative; height: auto; overflow: visible;"
|
||||
>
|
||||
<hc-upload v-if="myProfile" :user="user">
|
||||
<user-avatar :user="user" class="profile-avatar" size="large"></user-avatar>
|
||||
@ -87,12 +84,12 @@
|
||||
<ds-text color="soft" size="small" class="hyphenate-text">{{ user.about }}</ds-text>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
<ds-space />
|
||||
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px;">
|
||||
{{ $t('profile.network.title') }}
|
||||
</ds-heading>
|
||||
<ds-card style="position: relative; height: auto;">
|
||||
<base-card style="position: relative; height: auto;">
|
||||
<ds-space v-if="user.following && user.following.length" margin="x-small">
|
||||
<ds-text tag="h5" color="soft">
|
||||
{{ userName | truncate(15) }} {{ $t('profile.network.following') }}
|
||||
@ -120,9 +117,9 @@
|
||||
{{ userName }} {{ $t('profile.network.followingNobody') }}
|
||||
</p>
|
||||
</template>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
<ds-space />
|
||||
<ds-card style="position: relative; height: auto;">
|
||||
<base-card style="position: relative; height: auto;">
|
||||
<ds-space v-if="user.followedBy && user.followedBy.length" margin="x-small">
|
||||
<ds-text tag="h5" color="soft">
|
||||
{{ userName | truncate(15) }} {{ $t('profile.network.followedBy') }}
|
||||
@ -150,9 +147,9 @@
|
||||
{{ userName }} {{ $t('profile.network.followedByNobody') }}
|
||||
</p>
|
||||
</template>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
|
||||
<ds-card style="position: relative; height: auto;">
|
||||
<base-card style="position: relative; height: auto;">
|
||||
<ds-space margin="x-small">
|
||||
<ds-text tag="h5" color="soft">
|
||||
{{ $t('profile.socialMedia') }} {{ userName | truncate(15) }}?
|
||||
@ -166,14 +163,14 @@
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-space>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
|
||||
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
||||
<masonry-grid>
|
||||
<ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
|
||||
<ds-card class="ds-tab-nav">
|
||||
<base-card class="ds-tab-nav">
|
||||
<ul class="Tabs">
|
||||
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
|
||||
<a @click="handleTab('post')">
|
||||
@ -213,7 +210,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-grid-item>
|
||||
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth">
|
||||
@ -242,7 +239,7 @@
|
||||
:key="post.id"
|
||||
:imageAspectRatio="post.imageAspectRatio"
|
||||
>
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
@removePostFromList="removePostFromList"
|
||||
@ -275,7 +272,7 @@
|
||||
<script>
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import HcPostCard from '~/components/PostCard/PostCard.vue'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcFollowButton from '~/components/FollowButton.vue'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
import HcBadges from '~/components/Badges.vue'
|
||||
@ -303,7 +300,7 @@ const tabToFilterMapping = ({ tab, id }) => {
|
||||
export default {
|
||||
components: {
|
||||
UserTeaser,
|
||||
HcPostCard,
|
||||
PostTeaser,
|
||||
HcFollowButton,
|
||||
HcCountTo,
|
||||
HcBadges,
|
||||
@ -562,18 +559,17 @@ export default {
|
||||
top: 53px;
|
||||
z-index: 2;
|
||||
}
|
||||
.ds-tab-nav {
|
||||
.ds-card-content {
|
||||
padding: 0 !important;
|
||||
.ds-tab-nav-item {
|
||||
&.ds-tab-nav-item-active {
|
||||
border-bottom: 3px solid #17b53f;
|
||||
&:first-child {
|
||||
border-bottom-left-radius: $border-radius-x-large;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom-right-radius: $border-radius-x-large;
|
||||
}
|
||||
.ds-tab-nav.base-card {
|
||||
padding: 0;
|
||||
|
||||
.ds-tab-nav-item {
|
||||
&.ds-tab-nav-item-active {
|
||||
border-bottom: 3px solid #17b53f;
|
||||
&:first-child {
|
||||
border-bottom-left-radius: $border-radius-x-large;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom-right-radius: $border-radius-x-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,20 @@
|
||||
<template>
|
||||
<ds-container width="medium">
|
||||
<ds-card>
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }">
|
||||
<client-only>
|
||||
<locale-switch offset="5" />
|
||||
</client-only>
|
||||
<ds-space margin-top="small" margin-bottom="xxx-small">
|
||||
<img class="signup-image" alt="Human Connection" src="/img/sign-up/nicetomeetyou.svg" />
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<nuxt-child />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
<base-card>
|
||||
<template #imageColumn>
|
||||
<img alt="Human Connection" src="/img/sign-up/nicetomeetyou.svg" />
|
||||
</template>
|
||||
<nuxt-child />
|
||||
<template #topMenu>
|
||||
<locale-switch offset="5" />
|
||||
</template>
|
||||
</base-card>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LocaleSwitch,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-card :header="$t('settings.blocked-users.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.blocked-users.name') }}</h2>
|
||||
<ds-text>
|
||||
{{ $t('settings.blocked-users.explanation.intro') }}
|
||||
</ds-text>
|
||||
@ -16,9 +17,9 @@
|
||||
{{ $t('settings.blocked-users.explanation.notifications') }}
|
||||
</ds-list-item>
|
||||
</ds-list>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-space>
|
||||
<ds-card v-if="blockedUsers && blockedUsers.length">
|
||||
<base-card v-if="blockedUsers && blockedUsers.length">
|
||||
<ds-table :data="blockedUsers" :fields="fields" condensed>
|
||||
<template #avatar="scope">
|
||||
<nuxt-link
|
||||
@ -55,8 +56,8 @@
|
||||
<base-button circle size="small" @click="unblockUser(scope)" icon="user-plus" />
|
||||
</template>
|
||||
</ds-table>
|
||||
</ds-card>
|
||||
<ds-card v-else>
|
||||
</base-card>
|
||||
<base-card v-else>
|
||||
<ds-space>
|
||||
<ds-placeholder>
|
||||
{{ $t('settings.blocked-users.empty') }}
|
||||
@ -67,7 +68,7 @@
|
||||
{{ $t('settings.blocked-users.how-to') }}
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.download.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.download.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.embeds.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.embeds.name') }}</h2>
|
||||
<ds-section>
|
||||
<ds-text>
|
||||
{{ $t('settings.embeds.status.description') }}
|
||||
@ -31,7 +32,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</ds-section>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card :header="$t('settings.data.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
||||
<ds-input
|
||||
id="name"
|
||||
model="name"
|
||||
@ -30,12 +31,10 @@
|
||||
:label="$t('settings.data.labelBio')"
|
||||
:placeholder="$t('settings.data.labelBio')"
|
||||
/>
|
||||
<template slot="footer">
|
||||
<base-button icon="check" :disabled="errors" type="submit" :loading="loadingData" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</template>
|
||||
</ds-card>
|
||||
<base-button icon="check" :disabled="errors" type="submit" :loading="loadingData" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.invites.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.invites.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.languages.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.languages.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-card :header="$t('settings.muted-users.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.muted-users.name') }}</h2>
|
||||
<ds-text>
|
||||
{{ $t('settings.muted-users.explanation.intro') }}
|
||||
</ds-text>
|
||||
@ -13,9 +14,9 @@
|
||||
{{ $t('settings.muted-users.explanation.search') }}
|
||||
</ds-list-item>
|
||||
</ds-list>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-space>
|
||||
<ds-card v-if="mutedUsers && mutedUsers.length">
|
||||
<base-card v-if="mutedUsers && mutedUsers.length">
|
||||
<ds-table :data="mutedUsers" :fields="fields" condensed>
|
||||
<template #avatar="scope">
|
||||
<nuxt-link
|
||||
@ -52,8 +53,8 @@
|
||||
<base-button circle size="small" @click="unmuteUser(scope)" icon="user-plus" />
|
||||
</template>
|
||||
</ds-table>
|
||||
</ds-card>
|
||||
<ds-card v-else>
|
||||
</base-card>
|
||||
<base-card v-else>
|
||||
<ds-space>
|
||||
<ds-placeholder>
|
||||
{{ $t('settings.muted-users.empty') }}
|
||||
@ -64,7 +65,7 @@
|
||||
{{ $t('settings.muted-users.how-to') }}
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card :header="$t('settings.email.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.email.name') }}</h2>
|
||||
<ds-input
|
||||
id="email"
|
||||
model="email"
|
||||
@ -15,13 +16,10 @@
|
||||
icon="question-circle"
|
||||
:label="$t('settings.email.labelNonce')"
|
||||
/>
|
||||
|
||||
<template slot="footer">
|
||||
<base-button class="submit-button" icon="check" :disabled="errors" type="submit" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</template>
|
||||
</ds-card>
|
||||
<base-button icon="check" :disabled="errors" type="submit" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
@ -1,29 +1,27 @@
|
||||
<template>
|
||||
<ds-card centered v-if="data">
|
||||
<base-card v-if="data">
|
||||
<transition name="ds-transition-fade">
|
||||
<sweetalert-icon icon="info" />
|
||||
</transition>
|
||||
<ds-text v-html="submitMessage" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
<ds-form v-else v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card :header="$t('settings.email.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.email.name') }}</h2>
|
||||
<ds-input
|
||||
id="email"
|
||||
model="email"
|
||||
icon="envelope"
|
||||
:label="$t('settings.email.labelEmail')"
|
||||
/>
|
||||
|
||||
<template slot="footer">
|
||||
<ds-space class="backendErrors" v-if="backendErrors">
|
||||
<ds-text align="center" bold color="danger">{{ backendErrors.message }}</ds-text>
|
||||
</ds-space>
|
||||
<base-button icon="check" :disabled="errors" type="submit" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</template>
|
||||
</ds-card>
|
||||
<ds-space class="backendErrors" v-if="backendErrors">
|
||||
<ds-text align="center" bold color="danger">{{ backendErrors.message }}</ds-text>
|
||||
</ds-space>
|
||||
<base-button icon="check" :disabled="errors" type="submit" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-card>
|
||||
<base-card>
|
||||
<transition name="ds-transition-fade">
|
||||
<client-only>
|
||||
<sweetalert-icon :icon="sweetAlertIcon" />
|
||||
@ -34,7 +34,7 @@
|
||||
</client-only>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.organizations.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.organizations.name') }}</h2>
|
||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
@input-valid="handleInputValid"
|
||||
@submit="handleSubmitSocialMedia"
|
||||
>
|
||||
<ds-card :header="$t('settings.social-media.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.social-media.name') }}</h2>
|
||||
<ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small">
|
||||
<ds-list>
|
||||
<ds-list-item v-for="link in socialMediaLinks" :key="link.id" class="list-item--high">
|
||||
@ -62,7 +63,7 @@
|
||||
</base-button>
|
||||
</ds-space>
|
||||
</ds-space>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.privacy.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.privacy.name') }}</h2>
|
||||
<ds-space margin-bottom="small">
|
||||
<input id="allow-shouts" type="checkbox" v-model="shoutsAllowed" />
|
||||
<label for="allow-shouts">{{ $t('settings.privacy.make-shouts-public') }}</label>
|
||||
</ds-space>
|
||||
<base-button filled @click="submit" :disabled="disabled">{{ $t('actions.save') }}</base-button>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<ds-card :header="$t('settings.security.name')">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.security.name') }}</h2>
|
||||
<change-password />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,27 +1,21 @@
|
||||
<template>
|
||||
<ds-container width="medium">
|
||||
<ds-card icon="balance-scale" :header="$t(`termsAndConditions.newTermsAndConditions`)" centered>
|
||||
<p>
|
||||
<nuxt-link :to="{ name: 'terms-and-conditions' }" target="_blank">
|
||||
<base-button>
|
||||
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
|
||||
</base-button>
|
||||
</nuxt-link>
|
||||
</p>
|
||||
<ds-text>
|
||||
<input id="checkbox" type="checkbox" v-model="checked" :checked="checked" />
|
||||
<label
|
||||
for="checkbox"
|
||||
v-html="$t('termsAndConditions.termsAndConditionsNewConfirm')"
|
||||
></label>
|
||||
</ds-text>
|
||||
|
||||
<template slot="footer">
|
||||
<base-button filled @click="submit" :disabled="!checked">
|
||||
{{ $t(`actions.save`) }}
|
||||
<ds-container width="medium" class="terms-and-conditions-confirm">
|
||||
<base-card>
|
||||
<base-icon name="balance-scale" />
|
||||
<h2 class="title">{{ $t(`termsAndConditions.newTermsAndConditions`) }}</h2>
|
||||
<nuxt-link :to="{ name: 'terms-and-conditions' }" target="_blank">
|
||||
<base-button>
|
||||
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
|
||||
</base-button>
|
||||
</template>
|
||||
</ds-card>
|
||||
</nuxt-link>
|
||||
<label for="checkbox">
|
||||
<input id="checkbox" type="checkbox" v-model="checked" :checked="checked" />
|
||||
{{ $t('termsAndConditions.termsAndConditionsNewConfirm') }}
|
||||
</label>
|
||||
<base-button filled @click="submit" :disabled="!checked">
|
||||
{{ $t(`actions.save`) }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
@ -91,3 +85,17 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.terms-and-conditions-confirm > .base-card {
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
> .base-icon {
|
||||
font-size: $font-size-xxx-large;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user