Skip to content
This repository was archived by the owner on Dec 14, 2023. It is now read-only.

Commit 0523fd8

Browse files
authored
Feature/dojo events ics (#248)
* Add link to download ics * Hide the link if there are no future events * Fix linting * Fix unit test * Remove trace of the zoneName * Remove Beta now that I'm fairly sure time will work * Rework the UI Add unit tests & e2e tests * Add message describing the action taken while clicking the copy button * Add ga tracking * Update cp-translations * Add yarn lock
1 parent aa11098 commit 0523fd8

File tree

8 files changed

+198
-11
lines changed

8 files changed

+198
-11
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import page from '../../pages/dojo-details';
2+
3+
describe('Dojos details', () => {
4+
beforeEach(() => {
5+
cy.server();
6+
cy.route('/api/2.0/users/instance', 'fx:parentLoggedIn').as('loggedIn');
7+
});
8+
9+
it('should display the calendar', () => {
10+
cy.visit('/dojos/ie/dublin/dublin-ninja-kids');
11+
cy.wait('@loggedIn');
12+
cy.get(page.column.calendarLink).should('be.visible');
13+
cy.get(page.column.calendarLink).should('have.text', 'Add to your calendar');
14+
cy.get(page.column.calendarLink).click();
15+
cy.get(page.column.calendarInput).should('be.visible');
16+
cy.get(page.column.calendarCopyBtn).should('be.visible');
17+
cy.get(page.column.calendarOpenBtn).should('be.visible');
18+
});
19+
});

cypress/pages/dojo-details.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
export default {
22
name: '.cd-dojo-details__name',
3+
column: {
4+
calendarLink: '.cd-info-column-section.cd-dojo-details__left-column-section .cd-ics-link summary',
5+
calendarInput: '.cd-info-column-section.cd-dojo-details__left-column-section .cd-ics-link input',
6+
calendarCopyBtn: '.cd-info-column-section.cd-dojo-details__left-column-section .cd-ics-link button',
7+
calendarOpenBtn: '.cd-info-column-section.cd-dojo-details__left-column-section .cd-ics-link a',
8+
},
39
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@coderdojo/cd-common": "1.1.11",
2424
"bootstrap": "^3.4.1",
25-
"cp-translations": "1.0.126",
25+
"cp-translations": "1.0.127",
2626
"font-awesome": "^4.7.0",
2727
"handlebars": "^4.1.0",
2828
"js-cookie": "^2.1.4",

src/dojos/cd-dojo-details.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<info-column class="cd-dojo-details__left-column">
1414
<info-column-section class="cd-dojo-details__left-column-section" icon="clock-o" :header="$t('Time')">
1515
{{ buildDojoFrequency(dojoDetails) }}
16+
<ics-link :dojo-id="dojoDetails.id"/>
1617
</info-column-section>
1718
<info-column-section class="cd-dojo-details__left-column-section" icon="map-marker" :header="$t('Location')">
1819
{{ address }}
@@ -96,6 +97,7 @@
9697
import InfoColumn from '@/common/cd-info-column';
9798
import Dropdown from '@/common/cd-dropdown';
9899
import InfoColumnSection from '@/common/cd-info-column-section';
100+
import IcsLink from '@/events/cd-ics-link';
99101
import cdUrlFormatter from '@/common/filters/cd-url-formatter';
100102
import UserService from '@/users/service';
101103
import UsersDojosService from '@/usersDojos/service';
@@ -117,6 +119,7 @@
117119
eventsList,
118120
InfoColumn,
119121
InfoColumnSection,
122+
IcsLink,
120123
Dropdown,
121124
StaticMap,
122125
},

src/events/cd-event-list.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<template>
22
<div class="cd-event-list" >
3-
<div class="cd-event-list__heading" >{{ $t('Upcoming Events') }}</div>
3+
<div class="cd-event-list__heading" >
4+
<h4>{{ $t('Upcoming Events') }}</h4>
5+
</div>
46
<div v-if="!hasFutureEvents" class="cd-event-list__event">
57
<div class="cd-event-list__no-events">
68
<div class="cd-event-list__no-events-header" v-if="!hasPastEvents">
@@ -164,12 +166,17 @@
164166
165167
.cd-event-list {
166168
&__heading {
167-
color: #000;
168-
font-size: @font-size-large;
169-
margin: 0 0 16px 0;
170-
font-weight: bold;
169+
display: flex;
170+
justify-content: space-between;
171171
border-bottom: 1px solid #bebebe;
172-
padding-bottom: 8px;
172+
margin-bottom: 16px;
173+
h4 {
174+
color: #000;
175+
font-size: @font-size-large;
176+
margin: 0 0 8px 0;
177+
font-weight: bold;
178+
line-height: 1;
179+
}
173180
}
174181
&__event {
175182
border-style: solid;

src/events/cd-ics-link.vue

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<template>
2+
<div class="cd-ics-link">
3+
<details class="input-group">
4+
<summary ref="summary">{{ $t('Add to your calendar') }}</summary>
5+
<input type="text" name="httpUrl" :value="httpUrl" ref="httpUrl" class="form-control"/>
6+
<div class="input-group-append btn-group">
7+
<button name="copy" class="btn btn-default" v-ga-track-click="'ics-clipboard'" @click="toClipboard"><i class="fa fa-copy"></i></button>
8+
<a :href="webcalUrl" class="btn btn-default" name="open" role="button" v-ga-track-click="'ics-webcal'"><i class="fa fa-external-link"></i></a>
9+
</div>
10+
<p :class="[copied ? 'cd-ics-link__copy-label--hidden' : 'cd-ics-link__copy-label']">{{ $t('iCalendar feed copied!') }}</p>
11+
</details>
12+
</div>
13+
</template>
14+
<script>
15+
import moment from 'moment';
16+
17+
export default {
18+
name: 'IcsLink',
19+
props: ['dojoId'],
20+
data() {
21+
return {
22+
copied: false,
23+
};
24+
},
25+
methods: {
26+
toggleCopy() {
27+
this.copied = true;
28+
setTimeout(() => {
29+
this.copied = false;
30+
}, 2000);
31+
},
32+
toClipboard() {
33+
this.$refs.httpUrl.focus();
34+
this.$refs.httpUrl.select();
35+
// Clipboard API requires permission, this doesn't :O
36+
document.execCommand('copy');
37+
this.toggleCopy();
38+
},
39+
},
40+
computed: {
41+
currentDomain() {
42+
return window.origin;
43+
},
44+
host() {
45+
return window.location.host;
46+
},
47+
webcalUrl() {
48+
return `webcal://${this.host}${this.url}`;
49+
},
50+
httpUrl() {
51+
return `${this.currentDomain}${this.url}`;
52+
},
53+
url() {
54+
const afterDate = moment().unix();
55+
const utcOffset = moment().utcOffset();
56+
return `/api/3.0/dojos/${this.dojoId}/events.ics?query[status]=published&query[afterDate]=${afterDate}&query[utcOffset]=${utcOffset}`;
57+
},
58+
},
59+
};
60+
</script>
61+
<style scoped lang="less">
62+
.cd-ics-link {
63+
input {
64+
max-width: 170px;
65+
border-right-width: 0px;
66+
}
67+
.btn-group {
68+
.btn:first-child {
69+
border-top-left-radius: 0;
70+
border-bottom-left-radius: 0;
71+
}
72+
}
73+
&__copy-label {
74+
transition: opacity 1s ease-out;
75+
opacity: 0;
76+
overflow: hidden;
77+
position: relative;
78+
&--hidden {
79+
opacity: 1;
80+
}
81+
}
82+
}
83+
</style>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import vueUnitHelper from 'vue-unit-helper';
2+
import IcsLink from '@/events/cd-ics-link';
3+
import TimeShift from 'timeshift-js';
4+
5+
describe('ICS link component', () => {
6+
let sandbox;
7+
before(() => {
8+
sandbox = sinon.sandbox.create();
9+
});
10+
describe('computed', () => {
11+
describe('url', () => {
12+
it('should return the url to recover the dojo events', () => {
13+
const vm = vueUnitHelper(IcsLink);
14+
Date = TimeShift.Date; // eslint-disable-line no-global-assign
15+
TimeShift.setTimezoneOffset(-120); // UTC+2 (Italy, summer time)
16+
TimeShift.setTime(1501079400000); // 2017-07-26 16:30:00 GMT+02:00
17+
vm.dojoId = 1;
18+
19+
expect(vm.url).to.match(/\/api\/3\.0\/dojos\/1\/events\.ics\?query\[status\]=published&query\[afterDate\]=1501079400&query\[utcOffset\]=120/);
20+
});
21+
});
22+
describe('httpUrl', () => {
23+
it('should return the full url to recover the dojo events', () => {
24+
const vm = vueUnitHelper(IcsLink);
25+
vm.url = '/banana';
26+
expect(vm.httpUrl).to.match(/http:\/\/localhost:(\d)+\/banana/);
27+
});
28+
});
29+
describe('webcalUrl', () => {
30+
it('should return the full url to load using webcal', () => {
31+
const vm = vueUnitHelper(IcsLink);
32+
vm.url = '/banana';
33+
expect(vm.webcalUrl).to.match(/webcal:\/\/localhost:(\d)+\/banana/);
34+
});
35+
});
36+
});
37+
describe('methods', () => {
38+
describe('toClipboard', () => {
39+
it('should focus the input field and copy its content', () => {
40+
const vm = vueUnitHelper(IcsLink);
41+
vm.$refs = {
42+
httpUrl: {
43+
focus: sandbox.stub(),
44+
select: sandbox.stub(),
45+
},
46+
};
47+
vm.toggleCopy = sandbox.stub();
48+
const spy = sandbox.spy(document, 'execCommand');
49+
vm.toClipboard();
50+
expect(vm.$refs.httpUrl.focus).to.have.been.calledOnce;
51+
expect(vm.$refs.httpUrl.select).to.have.been.calledOnce;
52+
expect(spy).to.have.been.calledWith('copy');
53+
expect(vm.toggleCopy).to.have.been.calledOnce;
54+
});
55+
});
56+
describe('toggleCopy', () => {
57+
it('should switch copy over a 2 second period', (done) => {
58+
const vm = vueUnitHelper(IcsLink);
59+
vm.copied = false;
60+
vm.toggleCopy();
61+
expect(vm.copied).to.be.true;
62+
setTimeout(() => {
63+
expect(vm.copied).to.be.false;
64+
done();
65+
}, 2000);
66+
}).timeout(3000);
67+
});
68+
});
69+
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,10 +1988,10 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
19881988
parse-json "^2.2.0"
19891989
require-from-string "^1.1.0"
19901990

1991-
1992-
version "1.0.126"
1993-
resolved "https://registry.yarnpkg.com/cp-translations/-/cp-translations-1.0.126.tgz#1f3be1afa4838a854701ad7c893ad3b9cf18680d"
1994-
integrity sha512-OL1g2z+7qniZC+tawAgOir8qyiJkmYCcDALiFIB+hG+GJAzOY/Bm9KmtMqhf7ZesCCDqyyCdJ18m6vvBsVLEuQ==
1991+
1992+
version "1.0.127"
1993+
resolved "https://registry.yarnpkg.com/cp-translations/-/cp-translations-1.0.127.tgz#098101b97e42a060bf4bbbbeaa3ca320fc25f86b"
1994+
integrity sha512-ytwYky4bg36D9gIPPkz5IXKp6w20umBtaMp05E+DTKgDaNgO0rEZZQQ1oLLlF0cAyKssk6zkr0EG4ab8vDv+Dg==
19951995

19961996
crc32-stream@^2.0.0:
19971997
version "2.0.0"

0 commit comments

Comments
 (0)