#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">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<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`"/>
|
||||
<Icon v-else v="arrow-circle-right"/>
|
||||
<T v-if="$te(`calendar.events.${eventName}`)" :params="{param: eventParam}">calendar.events.{{eventName}}</T>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -12,7 +15,8 @@
|
|||
export default {
|
||||
props: {
|
||||
event: { required: true },
|
||||
range: {},
|
||||
year: { 'default': () => (new Date).getFullYear() },
|
||||
range: { type: Boolean },
|
||||
},
|
||||
computed: {
|
||||
eventName() {
|
||||
|
|
|
@ -25,9 +25,14 @@
|
|||
<p class="mb-0">
|
||||
iCalendar:
|
||||
</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"/>
|
||||
ICS
|
||||
<T>crud.download</T>
|
||||
.ics
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -56,10 +61,29 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
day: {},
|
||||
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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<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>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="input-group my-2">
|
||||
<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"/>
|
||||
</button>
|
||||
<a :href="link" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
|
@ -18,7 +18,7 @@
|
|||
link: { required: true },
|
||||
},
|
||||
mounted () {
|
||||
new ClipboardJS('.btn-clipboard');
|
||||
new ClipboardJS(this.$refs.clipboard);
|
||||
},
|
||||
methods: {
|
||||
focusLink() {
|
||||
|
|
|
@ -470,6 +470,8 @@ crud:
|
|||
author: 'Hinzugefügt von'
|
||||
saved: 'Änderungen erfolgreich gespeichert!'
|
||||
loginRequired: '{/account=Log dich ein} um Eintrag einzureichen'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -549,6 +549,8 @@ crud:
|
|||
author: 'Added by'
|
||||
saved: 'Changes saved successfully!'
|
||||
loginRequired: '{/account=Log in} to submit an entry'
|
||||
copy: 'Copy link'
|
||||
download: 'Download'
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -483,6 +483,8 @@ crud:
|
|||
author: 'Añadido por'
|
||||
saved: 'Los cambios se guardaron exitosamente!'
|
||||
loginRequired: '{/cuenta=Log in} para enviar un ítem'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -473,6 +473,8 @@ crud:
|
|||
author: 'Ajouté par'
|
||||
saved: 'Modifications enregistrées !'
|
||||
loginRequired: '{/compte=Connectez-vous} pour proposer un mot'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -462,6 +462,8 @@ crud:
|
|||
author: 'Toegevoegd door'
|
||||
saved: 'Veranderingen succesvol opgeslagen!'
|
||||
loginRequired: '{/account=Login} om een toevoeging in te dienen'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -472,6 +472,8 @@ crud:
|
|||
author: 'Lagt til av'
|
||||
saved: 'Endringer er lagret!'
|
||||
loginRequired: '{/account=Log in} for å'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -1191,6 +1191,8 @@ crud:
|
|||
author: 'Dodane przez'
|
||||
saved: 'Zmiany zapisane!'
|
||||
loginRequired: '{/konto=Zaloguj się}, aby zgłosić wpis'
|
||||
copy: 'Skopiuj link'
|
||||
download: 'Ściągnij'
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -480,6 +480,8 @@ crud:
|
|||
author: 'Adicionado por'
|
||||
saved: 'Alterações salvas com sucesso!'
|
||||
loginRequired: 'Faça {/conta=login} para participar do nosso banco de dados'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -446,6 +446,8 @@ crud:
|
|||
search: '搜索'
|
||||
author: '添加人'
|
||||
saved: '更改保存成功'
|
||||
copy: 'Copy link' # TODO
|
||||
download: 'Download' # TODO
|
||||
|
||||
footer:
|
||||
license: >
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"suml-loader": "^0.1.1",
|
||||
"twitter": "^1.7.1",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"vue-client-only": "^2.1.0",
|
||||
"vue-lazy-hydration": "^2.0.0-beta.4",
|
||||
"vue-matomo": "^3.13.5-0",
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<h3><T :params="{day: day.day}">calendar.dates.{{day.month}}</T> {{day.year}}</h3>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -8,26 +8,68 @@ import { clearLinkedText } from '../../src/helpers';
|
|||
|
||||
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();
|
||||
|
||||
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);
|
||||
if (!year) {
|
||||
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) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({error: 'Unexpected server error'});
|
||||
}
|
||||
renderEvents({[year.year]: [year.eventsByUuid[req.params.uuid]]}, res)
|
||||
}));
|
||||
|
||||
res.header('Content-type', 'text/calendar');
|
||||
res.send(value);
|
||||
})
|
||||
router.get(routeBase + '-:year.ics', handleErrorAsync(async (req, res) => {
|
||||
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;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const md5 = require("js-md5");
|
||||
const { v5: uuid5 } = require('uuid');
|
||||
|
||||
class Day {
|
||||
constructor(year, month, day, dayOfWeek) {
|
||||
|
@ -104,7 +105,11 @@ module.exports.Event = class {
|
|||
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 first = days[0];
|
||||
const last = days[days.length - 1].next();
|
||||
|
@ -123,6 +128,7 @@ module.exports.Event = class {
|
|||
start: [first.year, first.month, first.day],
|
||||
end: [last.year, last.month, last.day],
|
||||
calName: translations.calendar.headerLong,
|
||||
sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -194,13 +200,20 @@ class Year {
|
|||
for (let event of events) {
|
||||
for (let term of event.terms) {
|
||||
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) {
|
||||
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.eventsByUuid = {}
|
||||
for (let event of events) {
|
||||
this.eventsByUuid[event.getUuid()] = event;
|
||||
}
|
||||
}
|
||||
|
||||
isCurrent() {
|
||||
|
|
Reference in New Issue