#247 [calendar] ics - add all-time, just-one-event, add clipboard button
This commit is contained in:
parent
be09e8a3fd
commit
4bbdce77e4
|
@ -26,7 +26,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<li v-for="event in year.eventsByDate[d.toString()]" class="mb-2">
|
<li v-for="event in year.eventsByDate[d.toString()]" class="mb-2">
|
||||||
<CalendarEvent :event="event" :key="event.name"/>
|
<CalendarEvent :event="event" :year="year.year" :key="event.name"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<span v-if="range" class="badge bg-primary">{{ event.getRange(range) }}</span>
|
<span v-if="range" class="badge bg-primary">{{ event.getRange(year) }}</span>
|
||||||
<Flag v-if="event.flag" name="" alt="" :img="`/flags/${event.flag}.png`"/>
|
<Flag v-if="event.flag" name="" alt="" :img="`/flags/${event.flag}.png`"/>
|
||||||
<Icon v-else v="arrow-circle-right"/>
|
<Icon v-else v="arrow-circle-right"/>
|
||||||
<T v-if="$te(`calendar.events.${eventName}`)" :params="{param: eventParam}">calendar.events.{{eventName}}</T>
|
<T v-if="$te(`calendar.events.${eventName}`)" :params="{param: eventParam}">calendar.events.{{eventName}}</T>
|
||||||
<LinkedText v-else :text="eventName"/>
|
<LinkedText v-else :text="eventName"/>
|
||||||
|
<a :href="`/api/queer-calendar-${config.locale}-${year}-${event.getUuid()}.ics`" class="small" :aria-label="$t('crud.download') + ' .ics'" :title="$t('crud.download') + ' .ics'">
|
||||||
|
<Icon v="calendar-plus"/>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -12,7 +15,8 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
event: { required: true },
|
event: { required: true },
|
||||||
range: {},
|
year: { 'default': () => (new Date).getFullYear() },
|
||||||
|
range: { type: Boolean },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
eventName() {
|
eventName() {
|
||||||
|
|
|
@ -25,9 +25,14 @@
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
iCalendar:
|
iCalendar:
|
||||||
</p>
|
</p>
|
||||||
<a :href="`/api/calendar/queer-calendar-${year.year}.ics`" class="btn btn-outline-primary m-1">
|
<button :class="['btn', clipboardFeedback ? 'btn-success' : 'btn-outline-primary', 'm-1']" ref="clipboard" :data-clipboard-text="icsLink">
|
||||||
|
<Icon :v="clipboardFeedback ? 'clipboard-check' : 'clipboard'"/>
|
||||||
|
<T>crud.copy</T>
|
||||||
|
</button>
|
||||||
|
<a :href="icsLink" class="btn btn-outline-primary m-1">
|
||||||
<Icon v="calendar-plus"/>
|
<Icon v="calendar-plus"/>
|
||||||
ICS
|
<T>crud.download</T>
|
||||||
|
.ics
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -56,10 +61,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ClipboardJS from 'clipboard';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
day: {},
|
day: {},
|
||||||
year: {},
|
year: {},
|
||||||
}
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clipboardFeedback: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const clipboard = new ClipboardJS(this.$refs.clipboard);
|
||||||
|
clipboard.on('success', (e) => {
|
||||||
|
this.clipboardFeedback = true;
|
||||||
|
setTimeout(() => this.clipboardFeedback = false, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
icsLink() {
|
||||||
|
return `${process.env.BASE_URL}/api/queer-calendar-${this.config.locale}${this.year.year === (new Date).getFullYear() ? '' : '-' + this.year.year}.ics`;
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<li v-for="event in events" class="mb-2">
|
<li v-for="event in events" class="mb-2">
|
||||||
<CalendarEvent :event="event" :range="year.year" :key="event.name"/>
|
<CalendarEvent :event="event" :year="year.year" range :key="event.name"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="input-group my-2">
|
<div class="input-group my-2">
|
||||||
<input class="form-control" readonly :value="link.replace('https://', '').replace('http://', '')" id="link" ref="link">
|
<input class="form-control" readonly :value="link.replace('https://', '').replace('http://', '')" id="link" ref="link">
|
||||||
<button class="btn btn-outline-secondary btn-clipboard" data-clipboard-target="#link" :data-clipboard-text="link" @click="focusLink">
|
<button class="btn btn-outline-secondary" ref="clipboard" data-clipboard-target="#link" :data-clipboard-text="link" @click="focusLink" :aria-label="$t('crud.copy')" :title="$t('crud.copy')">
|
||||||
<Icon v="clipboard"/>
|
<Icon v="clipboard"/>
|
||||||
</button>
|
</button>
|
||||||
<a :href="link" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
<a :href="link" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
link: { required: true },
|
link: { required: true },
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
new ClipboardJS('.btn-clipboard');
|
new ClipboardJS(this.$refs.clipboard);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusLink() {
|
focusLink() {
|
||||||
|
|
|
@ -470,6 +470,8 @@ crud:
|
||||||
author: 'Hinzugefügt von'
|
author: 'Hinzugefügt von'
|
||||||
saved: 'Änderungen erfolgreich gespeichert!'
|
saved: 'Änderungen erfolgreich gespeichert!'
|
||||||
loginRequired: '{/account=Log dich ein} um Eintrag einzureichen'
|
loginRequired: '{/account=Log dich ein} um Eintrag einzureichen'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
@ -755,4 +757,4 @@ calendar:
|
||||||
overview: 'Überschau'
|
overview: 'Überschau'
|
||||||
labels: 'Etiketten'
|
labels: 'Etiketten'
|
||||||
link: 'Link'
|
link: 'Link'
|
||||||
full: 'Ganzer Kalender'
|
full: 'Ganzer Kalender'
|
||||||
|
|
|
@ -549,6 +549,8 @@ crud:
|
||||||
author: 'Added by'
|
author: 'Added by'
|
||||||
saved: 'Changes saved successfully!'
|
saved: 'Changes saved successfully!'
|
||||||
loginRequired: '{/account=Log in} to submit an entry'
|
loginRequired: '{/account=Log in} to submit an entry'
|
||||||
|
copy: 'Copy link'
|
||||||
|
download: 'Download'
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -483,6 +483,8 @@ crud:
|
||||||
author: 'Añadido por'
|
author: 'Añadido por'
|
||||||
saved: 'Los cambios se guardaron exitosamente!'
|
saved: 'Los cambios se guardaron exitosamente!'
|
||||||
loginRequired: '{/cuenta=Log in} para enviar un ítem'
|
loginRequired: '{/cuenta=Log in} para enviar un ítem'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -473,6 +473,8 @@ crud:
|
||||||
author: 'Ajouté par'
|
author: 'Ajouté par'
|
||||||
saved: 'Modifications enregistrées !'
|
saved: 'Modifications enregistrées !'
|
||||||
loginRequired: '{/compte=Connectez-vous} pour proposer un mot'
|
loginRequired: '{/compte=Connectez-vous} pour proposer un mot'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -462,6 +462,8 @@ crud:
|
||||||
author: 'Toegevoegd door'
|
author: 'Toegevoegd door'
|
||||||
saved: 'Veranderingen succesvol opgeslagen!'
|
saved: 'Veranderingen succesvol opgeslagen!'
|
||||||
loginRequired: '{/account=Login} om een toevoeging in te dienen'
|
loginRequired: '{/account=Login} om een toevoeging in te dienen'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -472,6 +472,8 @@ crud:
|
||||||
author: 'Lagt til av'
|
author: 'Lagt til av'
|
||||||
saved: 'Endringer er lagret!'
|
saved: 'Endringer er lagret!'
|
||||||
loginRequired: '{/account=Log in} for å'
|
loginRequired: '{/account=Log in} for å'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -1191,6 +1191,8 @@ crud:
|
||||||
author: 'Dodane przez'
|
author: 'Dodane przez'
|
||||||
saved: 'Zmiany zapisane!'
|
saved: 'Zmiany zapisane!'
|
||||||
loginRequired: '{/konto=Zaloguj się}, aby zgłosić wpis'
|
loginRequired: '{/konto=Zaloguj się}, aby zgłosić wpis'
|
||||||
|
copy: 'Skopiuj link'
|
||||||
|
download: 'Ściągnij'
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -480,6 +480,8 @@ crud:
|
||||||
author: 'Adicionado por'
|
author: 'Adicionado por'
|
||||||
saved: 'Alterações salvas com sucesso!'
|
saved: 'Alterações salvas com sucesso!'
|
||||||
loginRequired: 'Faça {/conta=login} para participar do nosso banco de dados'
|
loginRequired: 'Faça {/conta=login} para participar do nosso banco de dados'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -446,6 +446,8 @@ crud:
|
||||||
search: '搜索'
|
search: '搜索'
|
||||||
author: '添加人'
|
author: '添加人'
|
||||||
saved: '更改保存成功'
|
saved: '更改保存成功'
|
||||||
|
copy: 'Copy link' # TODO
|
||||||
|
download: 'Download' # TODO
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"suml-loader": "^0.1.1",
|
"suml-loader": "^0.1.1",
|
||||||
"twitter": "^1.7.1",
|
"twitter": "^1.7.1",
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"vue-client-only": "^2.1.0",
|
"vue-client-only": "^2.1.0",
|
||||||
"vue-lazy-hydration": "^2.0.0-beta.4",
|
"vue-lazy-hydration": "^2.0.0-beta.4",
|
||||||
"vue-matomo": "^3.13.5-0",
|
"vue-matomo": "^3.13.5-0",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<h3><T :params="{day: day.day}">calendar.dates.{{day.month}}</T> {{day.year}}</h3>
|
<h3><T :params="{day: day.day}">calendar.dates.{{day.month}}</T> {{day.year}}</h3>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<li v-for="event in year.eventsByDate[day.toString()]" class="mb-2">
|
<li v-for="event in year.eventsByDate[day.toString()]" class="mb-2">
|
||||||
<CalendarEvent :event="event" :key="event.name"/>
|
<CalendarEvent :event="event" :year="year.year" :key="event.name"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,26 +8,68 @@ import { clearLinkedText } from '../../src/helpers';
|
||||||
|
|
||||||
const translations = loadSuml('translations');
|
const translations = loadSuml('translations');
|
||||||
|
|
||||||
|
const renderEvents = (yearEvents, res) => {
|
||||||
|
const events = [];
|
||||||
|
let i = 1;
|
||||||
|
for (let year in yearEvents) {
|
||||||
|
if (!yearEvents.hasOwnProperty(year)) { continue; }
|
||||||
|
for (let event of yearEvents[year]) {
|
||||||
|
if (!event) { continue; }
|
||||||
|
const ics = event.toIcs(year, translations, clearLinkedText, i);
|
||||||
|
if (ics !== null) {
|
||||||
|
events.push(ics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
return res.status(404).json({error: 'Not found'});
|
||||||
|
}
|
||||||
|
|
||||||
|
createEvents(
|
||||||
|
events,
|
||||||
|
(error, value) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({error: 'Unexpected server error'});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.header('Content-type', 'text/calendar');
|
||||||
|
res.send(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/calendar/queer-calendar-:year.ics', handleErrorAsync(async (req, res) => {
|
const routeBase = `/queer-calendar-${global.config.locale}`;
|
||||||
|
|
||||||
|
router.get(routeBase + '.ics', handleErrorAsync(async (req, res) => {
|
||||||
|
let events = {};
|
||||||
|
for (let year of calendar.getAllYears()) {
|
||||||
|
events[year.year] = year.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEvents(events, res);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get(routeBase + '-:year-:uuid.ics', handleErrorAsync(async (req, res) => {
|
||||||
const year = calendar.getYear(req.params.year);
|
const year = calendar.getYear(req.params.year);
|
||||||
if (!year) {
|
if (!year) {
|
||||||
return res.status(404).json({error: 'Not found'});
|
return res.status(404).json({error: 'Not found'});
|
||||||
}
|
}
|
||||||
const events = year.events
|
|
||||||
.map(e => e.toIcs(req.params.year, translations, clearLinkedText))
|
|
||||||
.filter(e => e !== null);
|
|
||||||
|
|
||||||
createEvents(events, (error, value) => {
|
renderEvents({[year.year]: [year.eventsByUuid[req.params.uuid]]}, res)
|
||||||
if (error) {
|
}));
|
||||||
console.error(error);
|
|
||||||
return res.status(500).json({error: 'Unexpected server error'});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header('Content-type', 'text/calendar');
|
router.get(routeBase + '-:year.ics', handleErrorAsync(async (req, res) => {
|
||||||
res.send(value);
|
const year = calendar.getYear(req.params.year);
|
||||||
})
|
if (!year) {
|
||||||
|
return res.status(404).json({error: 'Not found'});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEvents({[year.year]: year.events}, res)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const md5 = require("js-md5");
|
const md5 = require("js-md5");
|
||||||
|
const { v5: uuid5 } = require('uuid');
|
||||||
|
|
||||||
class Day {
|
class Day {
|
||||||
constructor(year, month, day, dayOfWeek) {
|
constructor(year, month, day, dayOfWeek) {
|
||||||
|
@ -104,7 +105,11 @@ module.exports.Event = class {
|
||||||
return this.getDays(day.year)[0].equals(day);
|
return this.getDays(day.year)[0].equals(day);
|
||||||
}
|
}
|
||||||
|
|
||||||
toIcs(year, translations, clearLinkedText) {
|
getUuid() {
|
||||||
|
return uuid5(`${process.env.BASE_URL}/calendar/event/${this.name}`, uuid5.URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
toIcs(year, translations, clearLinkedText, sequence = 1) {
|
||||||
const days = this.getDays(year);
|
const days = this.getDays(year);
|
||||||
const first = days[0];
|
const first = days[0];
|
||||||
const last = days[days.length - 1].next();
|
const last = days[days.length - 1].next();
|
||||||
|
@ -123,6 +128,7 @@ module.exports.Event = class {
|
||||||
start: [first.year, first.month, first.day],
|
start: [first.year, first.month, first.day],
|
||||||
end: [last.year, last.month, last.day],
|
end: [last.year, last.month, last.day],
|
||||||
calName: translations.calendar.headerLong,
|
calName: translations.calendar.headerLong,
|
||||||
|
sequence,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,13 +200,20 @@ class Year {
|
||||||
for (let event of events) {
|
for (let event of events) {
|
||||||
for (let term of event.terms) {
|
for (let term of event.terms) {
|
||||||
if (this.eventsByTerm[term] === undefined) { this.eventsByTerm[term] = []; }
|
if (this.eventsByTerm[term] === undefined) { this.eventsByTerm[term] = []; }
|
||||||
this.eventsByTerm[term].push(event);
|
if (event.getDays(this.year).length) {
|
||||||
|
this.eventsByTerm[term].push(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let term in this.eventsByTerm) {
|
for (let term in this.eventsByTerm) {
|
||||||
if (!this.eventsByTerm.hasOwnProperty(term)) { continue; }
|
if (!this.eventsByTerm.hasOwnProperty(term)) { continue; }
|
||||||
this.eventsByTerm[term].sort((a, b) => a.getDays(this.year)[0].toInt() - b.getDays(this.year)[0].toInt())
|
this.eventsByTerm[term].sort((a, b) => a.getDays(this.year)[0].toInt() - b.getDays(this.year)[0].toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.eventsByUuid = {}
|
||||||
|
for (let event of events) {
|
||||||
|
this.eventsByUuid[event.getUuid()] = event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isCurrent() {
|
isCurrent() {
|
||||||
|
|
Reference in New Issue