...
 
Commits (117)
include:
template: Dependency-Scanning.gitlab-ci.yml
dependency_scanning:
rules:
- if: $CI_COMMIT_BRANCH == 'master'
when: always
- when: never
.defaults: &defaults
tags:
- shared
......@@ -83,7 +74,7 @@ outdated:
build:backend-chat:
<<: *defaults
rules:
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_BRANCH == 'production'
- if: $CI_MERGE_REQUEST_ID
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
......@@ -103,7 +94,7 @@ build:backend-chat:
build:frontend:
<<: *defaults
rules:
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_BRANCH == 'production'
- if: $CI_MERGE_REQUEST_ID
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
......@@ -124,7 +115,7 @@ build:frontend:
build:backend:
<<: *defaults
rules:
- if: $CI_COMMIT_BRANCH == 'master'
- if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_BRANCH == 'production'
- if: $CI_MERGE_REQUEST_ID
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
......
# Unreleased
## Major changes
## Features
- Sort own (managed) stores to top of topbar store list #920 !1546 @ChrisOelmueller
## Bugfixes
## Refactoring
- Move master-update function for regions to the rest controller !1547 @alex.simm
- Add missing endpoint for deleting forum threads !1545 #913 @alex.simm
## Dev/Test/CI stuff
# 2020-05-18 Hotfix
## Features
- Added tooltips to Dashboard Activities-Overview filter options !1526 @mr-kenhoff
## Bugfixes
- Be more robust against errors in the WebSocket Chat server: Let request suceed anyway. !1525 @NerdyProjects
......@@ -12,12 +24,18 @@
- Fix accessing null value as array in FairteilerView. !1527 @NerdyProjects
- Fix wrongly accessing null values in Fairteiler. !1527 @NerdyProjects
- Fix javascript error accessing the map the first time / without localstorage. !1528 @NerdyProjects
- Fix issuing invalid SQL IN() query !1534 @NerdyProjects
- Fix not logged in users getting errors when things should have been logged to their not-existing session !1531 @NerdyProjects
- Fix accessing invalid location for users without a session or without an address. !1538 @NerdyProjects
- Fix Content Security Policy violation for web worker for older browsers (fixes push notification for older browsers) @NerdyProjects
- Wrap long email address in user profile #828 !1541 @ChrisOelmueller
## Refactoring
## Dev/Test/CI stuff
- Migrate gitlab CI config to use rules instead of only/except !1529 @NerdyProjects
- Do not run CI tests before deployment !1529 @NerdyProjects
- Do not run gitlab dependency scanning job as nobody used the output !1533 @NerdyProjects
# 2020-05-16
......@@ -161,7 +179,18 @@
- added text about refactoring to devdocs @Caluera !1464
- added text about releases to devdocs @Caluera !1486
# 2020-04-22 Hotfix
- Use Geoapify as tile server and use mapbox gl to render vector tiles !1405 @dthulke
- More accurate email rate limiting !1419 @jofranz
- Set height for topbar and removed the height of div#main. Now is the broadcast message completely readable !1383 !1391 !1432 @chriswalg
- Improve the statistics for outgoing mail in grafana !1395 #64 @dthulke
- Fixed rendering error when replying to forum posts !1447 @ChrisOelmueller
# 2020-03-26 Hotfix
- Use WebSocket connection to determine whether a user is online or not !734 @janopae
- Adds a null check to the chat server to avoid null WebSocket messages !1398 @dthulke
* start documenting database tables and columns !1259 @flukx
# 2020-03-16 Hotfix
- Fix nightly fetcher warnings by using expected id instead of betrieb_id allowing all nightly maintenance methods to be executed again #747 !1348 @jofranz
......
......@@ -14,6 +14,5 @@ local function collect(user_key, session_prefix)
end
collect("php:user:" .. user_id .. ":sessions", "PHPREDIS_SESSION:")
collect("api:user:" .. user_id .. ":sessions", ":1:django.contrib.sessions.cache")
return all_session_ids
......@@ -9,7 +9,7 @@ export class RestifyServerFacade implements ServerFacade {
private readonly server: Server;
constructor () {
this.server = restify.createServer();
this.server = restify.createServer({ maxParamLength: 50000 });
this.server.use(bodyParser({ mapParams: false }));
}
......
......@@ -156,7 +156,7 @@ test('can send a message', t => {
});
});
test('can send to php users', t => {
test('can send to users', t => {
t.timeoutAfter(10000);
t.plan(3);
const sessionId = randomString.generate();
......@@ -175,25 +175,6 @@ test('can send to php users', t => {
});
});
test('can send to api users', t => {
t.timeoutAfter(10000);
t.plan(3);
const sessionId = randomString.generate();
const userId = 2;
addAPISessionToRedis(userId, sessionId, () => {
const socket = connect(t, sessionId, 'sessionid'); // django session cookie name
socket.on('some-app', (data: any) => {
t.equal(data.m, 'some-method', 'passed m param');
t.deepEqual(data.o, { someKey: 'some-payload' }, 'passed o param');
});
register(socket, () => {
sendMessage([userId], 'some-app', 'some-method', { someKey: 'some-payload' }, (err: any) => {
t.error(err, 'does not error');
});
});
});
});
test('works with two connections per user', t => {
t.timeoutAfter(20000);
......@@ -286,13 +267,15 @@ test('online status is false after user window moved into the background', t =>
const socket = connect(t, 'test-5-user-1');
register(socket, () => {
socket.emit('visibilitychange', true); // hidden = true
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
setTimeout(() => { // give the server some time to process the event
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
}, 100);
});
});
......@@ -303,14 +286,18 @@ test('online status is true after window came into the foreground again', t => {
const socket = connect(t, 'test-6-user-1');
register(socket, () => {
socket.emit('visibilitychange', true);
socket.emit('visibilitychange', false);
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
setTimeout(() => { // give the server some time to process the event
socket.emit('visibilitychange', false);
setTimeout(() => {
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
}, 100);
}, 100);
});
});
......@@ -323,14 +310,18 @@ test('online status is false if user has two windows and both are in the backgro
register(socket1, () => {
register(socket2, () => {
socket1.emit('visibilitychange', true);
socket2.emit('visibilitychange', true);
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
setTimeout(() => { // give the server some time to process the event
socket2.emit('visibilitychange', true);
setTimeout(() => {
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
}, 100);
}, 100);
});
});
});
......@@ -344,14 +335,18 @@ test('online status is true if user has two windows and only one is in the backg
register(socket1, () => {
register(socket2, () => {
socket1.emit('visibilitychange', false);
socket2.emit('visibilitychange', false);
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
setTimeout(() => { // give the server some time to process the event
socket2.emit('visibilitychange', false);
setTimeout(() => {
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
}, 100);
}, 100);
});
});
});
......@@ -366,14 +361,18 @@ test('online status is false if user has two windows in different browsers and b
register(socket1, () => {
register(socket2, () => {
socket1.emit('visibilitychange', true);
socket2.emit('visibilitychange', true);
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
setTimeout(() => { // give the server some time to process the event
socket2.emit('visibilitychange', true);
setTimeout(() => {
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, false, 'response body is "false"');
});
}, 100);
}, 100);
});
});
});
......@@ -388,14 +387,18 @@ test('online status is true if user has two windows in different browsers and on
register(socket1, () => {
register(socket2, () => {
socket1.emit('visibilitychange', true);
socket2.emit('visibilitychange', false);
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
setTimeout(() => { // give the server some time to process the event
socket2.emit('visibilitychange', false);
setTimeout(() => {
superagent.get(HTTP_URL + '/users/1/is-online').end((err, response) => {
if (err) {
t.error(err);
}
t.equal(response.type, 'application/json', 'content type is JSON');
t.equal(response.body, true, 'response body is "true"');
});
}, 100);
}, 100);
});
});
});
......@@ -426,7 +429,7 @@ function register (socket: Socket, callback: () => any): void {
}
function sendMessage (userIds: number[], channel: string, method: string, options: object, callback: (error: any, res: Response) => any): void {
superagent.post(HTTP_URL + `/users/${userIds.join('-')}/${channel}/${method}`).send(options).end(callback);
superagent.post(HTTP_URL + `/users/${userIds.join(',')}/${channel}/${method}`).send(options).end(callback);
}
function fetchStats (callback: (error: any, stats?: {connections: number, registrations: number, sessions: number}) => any): void {
......@@ -450,14 +453,6 @@ function addPHPSessionToRedis (userId: number, sessionId: string, callback: (err
.catch(error => callback(error));
}
function addAPISessionToRedis (userId: number, sessionId: string, callback: (error: any) => any): void {
redisClient.set(`:1:django.contrib.sessions.cache${sessionId}`, 'foo')
.then(async () =>
await redisClient.sadd(`api:user:${userId}:sessions`, sessionId)
).then(callback)
.catch(error => callback(error));
}
function assertStats (t: Test, connections: number, registrations: number, sessions: number, callback: (error?: any) => any): void {
fetchStats((err, stats) => {
if (err) return callback(err);
......
import { post } from './base'
import { patch, post } from './base'
export function join (regionId) {
return post(`/region/${regionId}/join`)
}
export function masterUpdate (regionId) {
return patch(`/region/${regionId}/masterupdate`)
}
<template>
<b-nav-item-dropdown
<fs-dropdown-menu
id="dropdown-baskets"
ref="dropdown"
v-b-tooltip="$i18n('basket.title')"
:no-caret="!showLabel"
menu-title="basket.title"
icon="fa-shopping-basket"
class="topbar-baskets"
>
<template v-slot:button-content>
<i class="fas fa-shopping-basket" />
<span v-if="showLabel">
<template v-slot:heading-text>
<span
v-if="showLabel"
class="d-none d-sm-inline-block"
>
{{ $i18n('basket.title') }}
</span>
<span v-else />
</template>
<div class="list-group">
<p
......@@ -46,17 +49,18 @@
</a>
</div>
</div>
</b-nav-item-dropdown>
</fs-dropdown-menu>
</template>
<script>
import MenuBasketsEntry from './MenuBasketsEntry'
import basketStore from '@/stores/baskets'
import FsDropdownMenu from '../FsDropdownMenu'
import { ajreq } from '@/script'
import dateFnsCompareDesc from 'date-fns/compareDesc'
export default {
components: { MenuBasketsEntry },
components: { MenuBasketsEntry, FsDropdownMenu },
props: {
showLabel: {
type: Boolean,
......
<template>
<b-nav-item-dropdown
<fs-dropdown-menu
id="dropdown-bells"
v-b-tooltip="$i18n('menu.entry.notifications')"
no-caret
menu-title="menu.entry.notifications"
icon="fa-bell"
right
class="topbar-bells"
:show-only-on-mobile="showOnlyOnMobile"
:hide-only-on-mobile="hideOnlyOnMobile"
>
<template v-slot:button-content>
<i class="fas fa-bell" />
<template v-slot:heading-text>
<span
v-if="unread"
class="badge badge-danger"
>
{{ unread }}
</span>
<span v-else />
</template>
<div class="list-group">
<small
......@@ -30,7 +32,7 @@
@bellClick="onBellClick"
/>
</div>
</b-nav-item-dropdown>
</fs-dropdown-menu>
</template>
<script>
import MenuBellsEntry from './MenuBellsEntry'
......@@ -38,9 +40,14 @@ import bellStore from '@/stores/bells'
import i18n from '@/i18n'
import { pulseError } from '@/script'
import dateFnsParseISO from 'date-fns/parseISO'
import FsDropdownMenu from '../FsDropdownMenu'
export default {
components: { MenuBellsEntry },
components: { MenuBellsEntry, FsDropdownMenu },
props: {
showOnlyOnMobile: { type: Boolean, default: false },
hideOnlyOnMobile: { type: Boolean, default: false }
},
computed: {
bells () {
return bellStore.bells.map(bell => {
......
<template>
<b-nav-item-dropdown
v-b-tooltip="$i18n(menuTitle)"
right
:lazy="lazy"
class="caret-beneath"
:class="{'d-md-none': showOnlyOnMobile, 'd-none d-md-inline-block': hideOnlyOnMobile }"
>
<template v-slot:button-content>
<i
v-if="icon"
:class="`fas ${icon}`"
/>
<slot name="heading-text">
<span
v-if="showMenuTitle"
class="d-md-none"
>{{ $i18n(menuTitle) }}</span>
</slot>
</template>
<slot>
<template v-for="heading in items">
<h3
:key="heading.heading"
class="dropdown-header"
>
{{ $i18n(heading.heading) }}
</h3>
<a
v-for="item in heading.menuItems"
:key="item.url"
:href="$url(item.url)"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n(item.menuTitle) }}
</a>
</template>
</slot>
</b-nav-item-dropdown>
</template>
<script>
export default {
props: {
menuTitle: {
type: String,
default: undefined
},
items: {
type: Array,
default: undefined
},
icon: {
type: String,
default: undefined
},
showMenuTitle: {
type: Boolean,
default: true
},
lazy: {
type: Boolean,
default: false
},
showOnlyOnMobile: { type: Boolean, default: false },
hideOnlyOnMobile: { type: Boolean, default: false }
}
}
</script>
<style lang="scss" scoped>
i {
font-size: 1rem;
}
.caret-beneath /deep/ .dropdown-toggle {
padding-bottom: 5px;
text-align: center;
&::after {
display: flex;
width: max-content;
align-self: center;
margin-left: auto;
margin-right: auto;
}
}
@media(max-width: 767px) {
.collapse .caret-beneath /deep/ .dropdown-toggle {
text-align: unset;
&::after {
display: inline-block;
margin-left: 0.255em;
vertical-align: middle;
}
}
}
.dropdown {
/deep/ .dropdown-menu {
max-height: 420px;
max-width: 300px;
overflow-y: auto;
box-shadow: 0 0 7px rgba(0, 0, 0, 0.3);
// Fixes problem that list of dropdown items is to long.
max-height: 70vh;
overflow: auto;
// Margin to have an indent in the burger menu.
margin-left: 30px;
.scroll-container {
max-height: 300px;
min-height: 120px;
overflow: auto;
}
}
@media (max-width: 575px) {
position: initial;
/deep/ .dropdown-menu {
width: 100%;
max-width: initial;
top: 2.2em;
.scroll-container {
width: 100%;
}
}
}
}
</style>
<template>
<b-navbar-nav
class="nav-row justify-content-md-center"
>
<menu-item
v-if="!hasFsRole"
:url="$url('upgradeToFs')"
icon="fa-rocket"
:title="$i18n('foodsaver.upgrade_to')"
:show-title-always="true"
/>
<menu-region
v-if="hasFsRole"
:regions="regions"
/>
<menu-groups
v-if="hasFsRole"
:working-groups="workingGroups"
/>
<menu-stores
v-if="hasFsRole"
:may-add-store="mayAddStore"
/>
<menu-baskets :show-label="!hasFsRole" />
<menu-item
:url="$url('map')"
icon="fa-map-marker-alt"
:title="$i18n('storelist.map')"
:hide-on-mobile="true"
:hide-title-always="true"
/>
<menu-messages :show-only-on-mobile="true" />
<menu-bells :show-only-on-mobile="true" />
<menu-user
:avatar="avatar"
:user-id="userId"
:show-only-on-mobile="true"
/>
<menu-item
v-if="hasFsRole"
id="search"
icon="fa-search"
:hide-title-always="true"
:show-only-on-mobile="true"
@click="$emit('openSearch')"
/>
</b-navbar-nav>
</template>
<script>
import MenuItem from './MenuItem'
import MenuRegion from './MenuRegion'
import MenuStores from './Stores/MenuStores'
import MenuGroups from './MenuGroups'
import MenuBaskets from './Baskets/MenuBaskets'
import MenuMessages from './Messages/MenuMessages'
import MenuBells from './Bells/MenuBells'
import MenuUser from './MenuUser'
export default {
components: {
MenuItem,
MenuRegion,
MenuStores,
MenuGroups,
MenuBaskets,
MenuMessages,
MenuBells,
MenuUser
},
props: {
hasFsRole: {
type: Boolean,
default: true
},
regions: {
type: Array,
default: () => []
},
workingGroups: {
type: Array,
default: () => []
},
mayAddStore: {
type: Boolean,
default: false
},
avatar: {
type: String,
default: ''
},
userId: {
type: Number,
default: null
}
}
}
</script>
<style lang="scss" scoped>
.bootstrap .navbar-nav /deep/ .dropdown-menu {
position: absolute;
}
</style>
<template>
<form
id="login-form"
class="form-inline my-2 my-lg-0 flex-grow-1"
@submit.prevent
<b-popover
target="login"
custom-class="bootstrap login-popover"
triggers="focus"
placement="top"
container="topbar"
variant="secondary"
@show="focusLogin=true"
@shown="focusLogin=false"
>
<div
ref="inputgroup"
class="input-group input-group-sm mr-2 my-1"
<form
id="login-form"
class="my-lg-0 flex-grow-1"
@submit.prevent
>
<div class="input-group-prepend">
<label
class="input-group-text text-primary"
for="login-email"
<div
ref="inputgroup"
class="input-group input-group-sm mr-2 my-1"
>
<div class="input-group-prepend">
<label
class="input-group-text text-primary"
for="login-email"
>
<i class="fas fa-user" />
</label>
</div>
<input
id="login-email"
ref="email"
v-model="email"
:placeholder="$i18n('login.email_address')"
:aria-label="$i18n('login.email_address')"
type="email"
name="login-email"
class="form-control text-primary"
@keydown.enter="submit"
>
<i class="fas fa-user" />
</label>
</div>
<input
id="login-email"
v-model="email"
:placeholder="$i18n('login.email_address')"
:aria-label="$i18n('login.email_address')"
type="email"
name="login-email"
class="form-control text-primary"
@keydown.enter="submit"
<div
ref="inputgroup"
class="input-group input-group-sm mr-2 my-1"
>
</div>
<div
ref="inputgroup"
class="input-group input-group-sm mr-2 my-1"
>
<div class="input-group-prepend">
<label
class="input-group-text text-primary"
for="login-password"
<div class="input-group-prepend">
<label
class="input-group-text text-primary"
for="login-password"
>
<i class="fas fa-key" />
</label>
</div>
<input
id="login-password"
v-model="password"
:placeholder="$i18n('login.password')"
:aria-label="$i18n('login.password')"
type="password"
name="login-password"
class="form-control text-primary"
autocomplete="on"
@keydown.enter="submit"
>
<i class="fas fa-key" />
</label>
</div>
<input
id="login-password"
v-model="password"
:placeholder="$i18n('login.password')"
:aria-label="$i18n('login.password')"
type="password"
name="login-password"
class="form-control text-primary"
@keydown.enter="submit"
<b-overlay
:show="isLoading"
>
</div>
<button
v-if="!isLoading "
:aria-label="$i18n('login.login_button_label')"
href="#"
class="btn btn-secondary btn-sm"
@click="submit"
>
<i class="fas fa-arrow-right" />
</button>
<button
v-else
:aria-label="$i18n('login.login_button_label')"
class="btn btn-light btn-sm loadingButton"
@click="submit"
>
<img src="/img/469.gif">
</button>
</form>
<template v-slot:overlay>
<img src="/img/469.gif">
</template>
<b-button
:aria-label="$i18n('login.login_button_label')"
href="#"
secondary
class="login-btn"
@click="submit"
>
<span>
Login
</span>
<i class="fas fa-arrow-right mr-auto" />
</b-button>
</b-overlay>
</form>
</b-popover>
</template>
<script>
......@@ -83,19 +98,28 @@ export default {
email: serverData.isDev ? '[email protected]' : '',
password: serverData.isDev ? 'user' : '',
isLoading: false,
error: null
error: null,
focusLogin: false
}
},
watch: {
focusLogin: function (val) {
if (val) {
this.$refs.email.focus()
this.$refs.email.select()
}
}
},
methods: {
async submit () {
if (!this.email) {
pulseError(i18n('login.error_no_email'))
window.location = this.$url('login')
// window.location = this.$url('login')
return
}
if (!this.password) {
pulseError(i18n('login.error_no_password'))
window.location = this.$url('login')
// window.location = this.$url('login')
return
}
this.isLoading = true
......@@ -115,13 +139,24 @@ export default {
if (err.code && err.code === 401) {
pulseError(i18n('login.error_no_auth'))
setTimeout(() => {
window.location = this.$url('login')
// window.location = this.$url('login')
}, 2000)
} else {
pulseError(i18n('error_unexpected'))
throw err
}
}
},
focusRef (ref) {
// Some references may be a component, functional component, or plain element
// This handles that check before focusing, assuming a `focus()` method exists
// We do this in a double `$nextTick()` to ensure components have
// updated & popover positioned first
this.$nextTick(() => {
this.$nextTick(() => {
;(ref.$el || ref).focus()
})
})
}
}
}
......@@ -134,9 +169,14 @@ export default {
}
}
#login-form .input-group {
@media (max-width: 575px) {
width: 80%;
.login-btn {
display: flex;
align-items: center;
span {
width: 100%;
}
}
.login-popover {
box-shadow: -0.5em 0.5em 20px -3px #333;
}
</style>
......@@ -2,9 +2,14 @@
<a
:href="linkUrl"
:aria-label="$i18n('home.title')"
class="navbar-brand mr-2"
class="navbar-brand brand"
>
food<span>shar<span>i</span>ng</span>
<span class="logo-text d-none d-md-inline-block">
food<span class="green">shar<span class="apple">i</span>ng</span>
</span>
<span class="logo-text d-md-none">
f<span class="green">s</span>
</span>
</a>
</template>
<script>
......@@ -18,32 +23,21 @@ export default {
}
</script>
<style lang="scss" scoped>
.navbar-brand {
.brand {
font-family: 'Alfa Slab One',serif;
color: #ffffff;
margin-right: 0;
margin-left: 5px;
font-size: 1.1rem;
span {
span.green {
color: #64ae25;
}
span {
position: relative;
&:hover {
span::before {
content: '♥';
color: red;
position: absolute;
font-size: 0.5em;
margin-top: -0.04em;
margin-left: -0.085em;
}
}
}
@media (max-width: 680px) {
font-size: 0.9rem;
&.small {
font-size: 0.4rem;
}
span.logo-text:hover .apple::before {
content: '♥';
color: red;
position: absolute;
font-size: 0.5em;
}
}
</style>
<template>
<b-nav-item-dropdown
<fs-dropdown-menu
id="dropdown-admin"
v-b-tooltip="$i18n('menu.entry.administration')"
right
no-caret
ref="dropdown"
menu-title="menu.entry.administration"
icon="fa-cog"
>
<template v-slot:button-content>
<i class="fas fa-cog" />
<span class="d-md-none">
{{ $i18n('menu.entry.administration') }}
</span>
</template>
<b-dropdown-item
v-for="item in items"
:key="item.url"
......@@ -18,10 +12,12 @@
>
<i :class="item.icon" /> {{ item.label }}
</b-dropdown-item>
</b-nav-item-dropdown>
</fs-dropdown-menu>
</template>
<script>
import FsDropdownMenu from './FsDropdownMenu'
export default {
components: { FsDropdownMenu },
props: {
may: {
type: Object,
......
<template>
<b-nav-item-dropdown
id="dropdown-bullhorn"
v-b-tooltip="$i18n('menu.entry.activities')"
:no-caret="!displayArrow"
right
>
<template v-slot:button-content>
<i class="fas fa-bullhorn" />
<span v-if="displayText">
{{ $i18n('menu.entry.activities') }}
</span>
</template>
<h3 class="dropdown-header">
{{ $i18n('menu.entry.politics') }}
</h3>
<a
:href="$url('fsstaedte')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.fscities') }}
</a>
<a
:href="$url('claims')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.demands') }}
</a>
<a
:href="$url('leeretonne')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.pastcampaigns') }}
</a>
<h3 class="dropdown-header">
{{ $i18n('menu.entry.education') }}
</h3>
<a
:href="$url('academy')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.academy') }}
</a>
<a
:href="$url('workshops')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.talksandworkshops') }}
</a>
<a
:href="$url('festival')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.fsfestival') }}
</a>
</b-nav-item-dropdown>
<fs-dropdown-menu
menu-title="menu.entry.activities"
:items="headings"
icon="fa-bullhorn"
/>
</template>
<script>
import FsDropdownMenu from './FsDropdownMenu'
export default {
props: {
displayArrow: {
type: Boolean,
default: true
},
displayText: {
type: Boolean,
default: false
components: {
FsDropdownMenu
},
data () {
return {
headings: [
{
heading: 'menu.entry.politics',
menuItems: [
{ url: 'fsstaedte', menuTitle: 'menu.entry.fscities' },
{ url: 'claims', menuTitle: 'menu.entry.demands' },
{ url: 'leeretonne', menuTitle: 'menu.entry.pastcampaigns' }
]
},
{
heading: 'menu.entry.education',
menuItems: [
{ url: 'academy', menuTitle: 'menu.entry.academy' },
{ url: 'workshops', menuTitle: 'menu.entry.talksandworkshops' },
{ url: 'festival', menuTitle: 'menu.entry.fsfestival' }
]
}
]
}
}
}
......
<template>
<b-nav-item-dropdown
id="dropdown-envelope"
v-b-tooltip="$i18n('menu.entry.contact')"
:no-caret="!displayArrow"
right
>
<template v-slot:button-content>
<i class="fas fa-envelope" />
<span class="sr-only">{{ $i18n('menu.entry.contact') }}</span>
<span v-if="displayText">{{ $i18n('menu.entry.contact') }}</span>
</template>
<b-dropdown-item
v-if="displayMailbox"
:href="$url('mailbox')"
>
{{ $i18n('menu.entry.mailbox') }}
</b-dropdown-item>
<b-dropdown-divider
v-if="displayMailbox"
/>
<b-dropdown-item
:href="$url('contact')"
>
{{ $i18n('menu.entry.contact') }}
</b-dropdown-item>
<b-dropdown-item
:href="$url('donate')"
>
{{ $i18n('menu.entry.donate') }}
</b-dropdown-item>
<b-dropdown-item
:href="$url('press')"
>
{{ $i18n('menu.entry.press') }}
</b-dropdown-item>
<b-dropdown-item
:href="$url('infosCompany')"
>
{{ $i18n('menu.entry.forcompanies') }}
</b-dropdown-item>
<b-dropdown-item
:href="$url('imprint')"
>
{{ $i18n('menu.entry.imprint') }}
</b-dropdown-item>
<b-dropdown-divider />
<b-dropdown-header
id="dropdown-header-groups"
>
{{ $i18n('menu.entry.regionalgroups') }}
</b-dropdown-header>
<a
:href="$url('communitiesGermany')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.Germany') }}
</a>
<a
:href="$url('communitiesAustria')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.Austria') }}
</a>
<a
:href="$url('communitiesSwitzerland')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.Swiss') }}
</a>
<a
:href="$url('international')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.international') }}
</a>
</b-nav-item-dropdown>
<fs-dropdown-menu
menu-title="menu.entry.contact"
:items="headings"
icon="fa-envelope"
/>
</template>
<script>
import { BDropdownDivider, BDropdownItem, BNavItemDropdown } from 'bootstrap-vue'
<script>
import FsDropdownMenu from './FsDropdownMenu'
export default {
components: { BDropdownDivider, BDropdownItem, BNavItemDropdown },
components: { FsDropdownMenu },
props: {
displayArrow: {
type: Boolean,
......@@ -99,6 +24,33 @@ export default {
type: Boolean,
default: false
}
},
data () {
const contactMenuItems = []
if (this.displayMailbox) {
contactMenuItems.push({ url: 'mailbox', menuTitle: 'menu.entry.mailbox' })
}
contactMenuItems.push({ url: 'contact', menuTitle: 'menu.entry.contact' })
contactMenuItems.push({ url: 'donate', menuTitle: 'menu.entry.donate' })
contactMenuItems.push({ url: 'press', menuTitle: 'menu.entry.press' })
contactMenuItems.push({ url: 'infosCompany', menuTitle: 'menu.entry.forcompanies' })
contactMenuItems.push({ url: 'imprint', menuTitle: 'menu.entry.imprint' })
return {
headings: [{
heading: 'menu.entry.contact',
menuItems: contactMenuItems
},
{
heading: 'menu.entry.regionalgroups',
menuItems: [
{ url: 'communitiesGermany', menuTitle: 'menu.entry.Germany' },
{ url: 'communitiesAustria', menuTitle: 'menu.entry.Austria' },
{ url: 'communitiesSwitzerland', menuTitle: 'menu.entry.Swiss' },
{ url: 'international', menuTitle: 'menu.entry.international' }
]
}]
}
}
}
</script>
<template>
<div>
<b-nav-item-dropdown
<fs-dropdown-menu
v-if="workingGroups.length"
id="dropdown-groups"
v-b-tooltip="$i18n('menu.entry.your_groups')"
no-caret
menu-title="menu.entry.your_groups"
:show-menu-title="false"
icon="fa-users"
>
<template v-slot:button-content>
<i class="fas fa-users" />
</template>
<div
v-for="group in workingGroups"
:key="group.id"
......@@ -83,29 +81,23 @@
>
<small><i class="fas fa-users" /> {{ $i18n('menu.entry.groups') }}</small>
</a>
</b-nav-item-dropdown>
<li
</fs-dropdown-menu>
<menu-item
v-else
v-b-tooltip
:url="$url('workingGroups')"
icon="fa-users"
:title="$i18n('menu.entry.groups')"
class="nav-item"
>
<a
v-b-tooltip
:title="$i18n('menu.entry.groups')"
:href="$url('workingGroups')"
class="nav-link"
>
<i class="fas fa-users" />
</a>
</li>
:hide-title-mobile="true"
/>
</div>
</template>
<script>
import { BCollapse, VBToggle, VBTooltip } from 'bootstrap-vue'
import FsDropdownMenu from './FsDropdownMenu'
import MenuItem from './MenuItem'
export default {
components: { BCollapse },
components: { BCollapse, FsDropdownMenu, MenuItem },
directives: { VBToggle, VBTooltip },
props: {
workingGroups: {
......
<template>
<b-nav-item-dropdown
id="dropdown-information"
v-b-tooltip="$i18n('menu.entry.infos')"
:no-caret="!displayArrow"
right
>
<template v-slot:button-content>
<i class="fas fa-info " />
<span v-if="displayText">
{{ $i18n('menu.entry.infos') }}
</span>
</template>
<h3 class="dropdown-header">
{{ $i18n('menu.entry.aboutUs') }}
</h3>
<a
:href="$url('mission')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.mission') }}
</a>
<a
:href="$url('grundsaetze')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.fundamentals') }}
</a>
<a
:href="$url('blog')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.blog') }}
</a>
<a
:href="$url('team')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.team') }}
</a>
<a
:href="$url('partner')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.partners') }}
</a>
<h3 class="dropdown-header">
{{ $i18n('menu.entry.background') }}
</h3>
<a
:href="$url('faq')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.faq') }}
</a>
<a
:href="$url('wiki')"
class="dropdown-item sub"
target="_blank"
rel="noopener noreferrer nofollow"
role="menuitem"
>
{{ $i18n('menu.entry.wiki') }}
</a>
<a
:href="$url('guide')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.guide') }}
</a>
<a
:href="$url('statistics')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.statistics') }}
</a>
<a
:href="$url('transparency')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.transparency') }}
</a>
<a
:href="$url('dataprivacy')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.dataprivacy') }}
</a>
<a
:href="$url('releaseNotes')"
class="dropdown-item sub"
role="menuitem"
>
{{ $i18n('menu.entry.release-notes') }}
</a>
</b-nav-item-dropdown>
<fs-dropdown-menu
menu-title="menu.entry.infos"
:items="headings"
icon="fa-info"
/>
</template>
<script>
import FsDropdownMenu from './FsDropdownMenu'
export default {
components: { FsDropdownMenu },
props: {
wXS: {
type: Boolean,
......@@ -121,6 +23,35 @@ export default {
type: Boolean,
default: false
}
},
data () {
return {
headings:
[
{
heading: 'menu.entry.aboutUs',
menuItems: [
{ url: 'mission', menuTitle: 'menu.entry.mission' },
{ url: 'grundsaetze', menuTitle: 'menu.entry.fundamentals' },
{ url: 'blog', menuTitle: 'menu.entry.blog' },
{ url: 'team', menuTitle: 'menu.entry.team' },
{ url: 'partner', menuTitle: 'menu.entry.partners' }
]
},
{
heading: 'menu.entry.background',
menuItems: [
{ url: 'faq', menuTitle: 'menu.entry.faq' },
{ url: 'wiki', menuTitle: 'menu.entry.wiki' },
{ url: 'guide', menuTitle: 'menu.entry.guide' },
{ url: 'statistics', menuTitle: 'menu.entry.statistics' },
{ url: 'transparency', menuTitle: 'menu.entry.transparency' },
{ url: 'dataprivacy', menuTitle: 'menu.entry.dataprivacy' },
{ url: 'releaseNotes', menuTitle: 'menu.entry.release-notes' }
]
}
]
}
}
}
</script>
<template>
<b-nav-item
v-b-tooltip.hover.bottom
:class="{'d-none d-md-inline-block': hideOnMobile, 'd-md-none': showOnlyOnMobile}"
:href="url"
:title="title"
:aria-label="title"
@click="$emit('click')"
>
<i :class="`fas ${icon}`" />
<span
v-if="!hideTitleAlways"
:class="{'d-md-none': !showTitleAlways, 'd-none d-md-inline-block': hideTitleMobile}"
>
{{ title }}
</span>
</b-nav-item>
</template>