#266 [calendar] multiple years, day view

This commit is contained in:
Avris 2021-10-03 16:37:00 +02:00
parent 2b799b516c
commit 0b6feb406a
18 changed files with 234 additions and 77 deletions

View File

@ -1,43 +1,51 @@
<template> <template>
<div class="calendar"> <div class="calendar">
<div v-for="i in (startingDayOfWeek - 1)"></div> <div v-for="i in (startingDayOfWeek - 1)"></div>
<div v-for="d in iterateMonth(year, month)" <component :is="tooltips || getDayClass(d) === 'day' ? 'div' : 'nuxt-link'"
:class="['rounded-circle', getDayClass(d), markToday && d.equals(today) ? 'day-today' : '', d.equals(selectedDay) ? 'day-selected' : '']" v-for="d in iterateMonth(year.year, month)" :key="d.toString()"
@click.stop="selectDay(d)" :to="`/${config.calendar.route}/${d}`"
:data-flag="getDayFlag(d)" :class="['rounded-circle', getDayClass(d), mark && d.equals(mark) ? 'day-today' : '', d.equals(selectedDay) ? 'day-selected' : '']"
:style="getDayFlag(d) ? `background-image: url('${getDayFlag(d)}')` : ''" @click.stop="selectDay(d)"
:data-flag="getDayFlag(d)"
:style="getDayFlag(d) ? `background-image: url('${getDayFlag(d)}')` : ''"
> >
<div class="day-number">{{ d.day }}</div> <div class="day-number">{{ d.day }}</div>
<div v-if="currentYear.eventsByDate[d.toString()] !== undefined && d.equals(selectedDay)" class="day-tooltip card text-dark shadow"> <div v-if="tooltips && year.eventsByDate[d.toString()] !== undefined && d.equals(selectedDay)" class="day-tooltip card text-dark shadow">
<div class="card-header d-flex justify-content-between"> <div class="card-header d-flex justify-content-between">
<p class="h5 mb-0"><strong><T :params="{day: d.day}">calendar.dates.{{d.month}}</T></strong></p> <p class="h5 mb-0"><strong><T :params="{day: d.day}">calendar.dates.{{d.month}}</T></strong></p>
<button class="btn btn-sm py-0" @clik="selectedDay = null"><Icon v="times"/></button> <span>
<nuxt-link :to="`/${config.calendar.route}/${d}`">
<Icon v="link"/>
<T>calendar.link</T>
</nuxt-link>
<button class="btn btn-sm py-0" @clik="selectedDay = null">
<Icon v="times"/>
</button>
</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<CalendarEvent v-for="event in currentYear.eventsByDate[d.toString()]" :event="event" :key="event.name"/> <CalendarEvent v-for="event in year.eventsByDate[d.toString()]" :event="event" :key="event.name"/>
</ul> </ul>
</div> </div>
</div> </div>
</div> </component>
</div> </div>
</template> </template>
<script> <script>
import { Day, iterateMonth, EventLevel } from '../src/calendar/helpers'; import { Day, iterateMonth, EventLevel } from '../src/calendar/helpers';
import { currentYear } from '../src/calendar/calendar';
export default { export default {
props: { props: {
year: { required: true }, year: { required: true },
month: { required: true }, month: { required: true },
markToday: { type: Boolean }, mark: { },
tooltips: { type: Boolean },
}, },
data() { data() {
return { return {
iterateMonth, iterateMonth,
currentYear,
today: Day.today(),
selectedDay: null, selectedDay: null,
} }
}, },
@ -60,17 +68,17 @@
}, },
computed: { computed: {
startingDayOfWeek() { startingDayOfWeek() {
return new Day(this.year, this.month, 1).dayOfWeek; return new Day(this.year.year, this.month, 1).dayOfWeek;
} }
}, },
methods: { methods: {
getDayClass(d) { getDayClass(d) {
if (this.currentYear.eventsByDate[d.toString()] === undefined) { if (this.year.eventsByDate[d.toString()] === undefined) {
return 'day'; return 'day';
} }
let maxLevel = 0; let maxLevel = 0;
for (let event of this.currentYear.eventsByDate[d.toString()]) { for (let event of this.year.eventsByDate[d.toString()]) {
if (event.level > maxLevel) { if (event.level > maxLevel) {
maxLevel = event.level; maxLevel = event.level;
} }
@ -79,7 +87,7 @@
return `day day-event day-event-${maxLevel}`; return `day day-event day-event-${maxLevel}`;
}, },
getDayFlag(d) { getDayFlag(d) {
for (let event of (this.currentYear.eventsByDate[d.toString()] || []).filter(e => e.level === EventLevel.Day && e.flag)) { for (let event of (this.year.eventsByDate[d.toString()] || []).filter(e => e.level === EventLevel.Day && e.flag)) {
return `/flags/${event.flag}.png`; return `/flags/${event.flag}.png`;
} }
return null; return null;
@ -90,6 +98,9 @@
} }
}, },
selectDay(d) { selectDay(d) {
if (!this.tooltips) {
return;
}
if (d.equals(this.selectedDay)) { if (d.equals(this.selectedDay)) {
this.selectedDay = null; this.selectedDay = null;
} else { } else {

View File

@ -15,16 +15,17 @@
</template> </template>
<script> <script>
import { currentYear } from '../src/calendar/calendar'; import { calendar } from '../src/calendar/calendar';
import { Day } from '../src/calendar/helpers'; import { Day } from '../src/calendar/helpers';
export default { export default {
props: { props: {
day: { 'default': () => Day.today() },
link: { type: Boolean }, link: { type: Boolean },
}, },
data() { data() {
return { return {
events: currentYear.eventsByDate[Day.today().toString()], events: calendar.getCurrentYear().eventsByDate[this.day.toString()],
} }
} }
} }

View File

@ -0,0 +1,49 @@
<template>
<section>
<div class="alert alert-info d-flex flex-column flex-md-row justify-content-around">
<div v-if="day">
<p class="mb-0">
<T>calendar.full</T>:
</p>
<nuxt-link :to="`/${config.calendar.route}/${day.year}`" class="btn btn-outline-primary m-1">
<Icon v="calendar-star"/>
{{ day.year }}
</nuxt-link>
</div>
<div>
<p class="mb-0">
Twitter Bot:
</p>
<p class="mb-0">
<a href="https://twitter.com/CalendarQueer" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="twitter" set="b"/>
@CalendarQueer
</a>
</p>
</div>
<div>
<p class="mb-0">
<T>calendar.image.header</T>:
</p>
<p class="mb-0">
<a href="/calendar/overview.png" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="image"/>
<T>calendar.image.overview</T>
</a>
<a href="/calendar/labels.png" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="image"/>
<T>calendar.image.labels</T>
</a>
</p>
</div>
</div>
</section>
</template>
<script>
export default {
props: {
day: {},
}
}
</script>

View File

@ -1,12 +1,11 @@
<template> <template>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<CalendarEvent v-for="event in events" :event="event" :range="year" :key="event.name"/> <CalendarEvent v-for="event in events" :event="event" :range="year.year" :key="event.name"/>
</ul> </ul>
</template> </template>
<script> <script>
import { iterateMonth } from '../src/calendar/helpers'; import { iterateMonth } from '../src/calendar/helpers';
import { currentYear } from '../src/calendar/calendar';
export default { export default {
props: { props: {
@ -16,8 +15,8 @@
computed: { computed: {
events() { events() {
let events = []; let events = [];
for (let day of iterateMonth(this.year, this.month)) { for (let day of iterateMonth(this.year.year, this.month)) {
for (let event of currentYear.eventsByDate[day.toString()] || []) { for (let event of this.year.eventsByDate[day.toString()] || []) {
if (event.isFirstDay(day)) { if (event.isFirstDay(day)) {
events.push(event); events.push(event);
} }

View File

@ -36,7 +36,8 @@
if (current.includes('#')) { if (current.includes('#')) {
current = current.substring(0, current.indexOf('#')); current = current.substring(0, current.indexOf('#'));
} }
return current === this.buildRoute(route).replace(/\/$/, ''); const expected = this.buildRoute(route).replace(/\/$/, '');
return current === expected || current.startsWith(expected + '/');
}, },
}, },
} }

View File

@ -741,3 +741,5 @@ calendar:
header: 'Bild herunterladen' header: 'Bild herunterladen'
overview: 'Überschau' overview: 'Überschau'
labels: 'Etiketten' labels: 'Etiketten'
link: 'Link' # TODO
full: 'Full calendar' # TODO

View File

@ -720,3 +720,5 @@ calendar:
header: 'Download an image' header: 'Download an image'
overview: 'Overview' overview: 'Overview'
labels: 'Labels' labels: 'Labels'
link: 'Link'
full: 'Full calendar'

View File

@ -755,3 +755,5 @@ calendar:
header: 'Descargar una imagen' header: 'Descargar una imagen'
overview: 'Vista general' overview: 'Vista general'
labels: 'Etiquetas' labels: 'Etiquetas'
link: 'Link' # TODO
full: 'Full calendar' # TODO

View File

@ -219,7 +219,7 @@ faq:
Aangezien we al vragen hoe iemand heet, waarom dan ook niet wat hun voornaamwoorden zijn? Aangezien we al vragen hoe iemand heet, waarom dan ook niet wat hun voornaamwoorden zijn?
- > - >
(Formuleer het alsjeblieft alleen niet als "ben je een jongen of een meisje?" (Formuleer het alsjeblieft alleen niet als "ben je een jongen of een meisje?"
Die vraag impliceert dat er alleen twee correcte antwoorden zijn Die vraag impliceert dat er alleen twee correcte antwoorden zijn
en het suggereert dat je een ongezonde nieuwsgierigheid hebt wat betreft de genitaliën van die persoon. en het suggereert dat je een ongezonde nieuwsgierigheid hebt wat betreft de genitaliën van die persoon.
In plaats daarvan zou je gewoon kunnen vragen: "Wat zijn je voornaamwoorden?" of "hoe wil je aangesproken worden?") In plaats daarvan zou je gewoon kunnen vragen: "Wat zijn je voornaamwoorden?" of "hoe wil je aangesproken worden?")
- > - >
@ -237,7 +237,7 @@ faq:
question: 'Worden deze voornaamwoorden überhaupt door iemand gebruikt?' question: 'Worden deze voornaamwoorden überhaupt door iemand gebruikt?'
answer: answer:
- > - >
Ja! Door miljoenen non-binaire personen over de hele wereld. Ja! Door miljoenen non-binaire personen over de hele wereld.
Voor elk van de voornaamwoorden dat hier beschreven staat is er iemand die ze ook daadwerkelijk gebruikt in het dagelijks leven. Voor elk van de voornaamwoorden dat hier beschreven staat is er iemand die ze ook daadwerkelijk gebruikt in het dagelijks leven.
authority: authority:
question: 'Are those nonbinary pronouns approved by some kind of authority?' question: 'Are those nonbinary pronouns approved by some kind of authority?'
@ -252,7 +252,7 @@ faq:
Dictionaries take their time to start including those changes, Dictionaries take their time to start including those changes,
which doesn't make the change illegitimate in any way. which doesn't make the change illegitimate in any way.
But eventually the new forms, if used often enough, get included in dictionaries. But eventually the new forms, if used often enough, get included in dictionaries.
The American dictionary {https://www.merriam-webster.com/words-at-play/singular-nonbinary-they=Merriam Webster}, The American dictionary {https://www.merriam-webster.com/words-at-play/singular-nonbinary-they=Merriam Webster},
for example, accepts the use of {/they=singular “they”} as a nonbinary pronoun. for example, accepts the use of {/they=singular “they”} as a nonbinary pronoun.
- > - >
You can also read some {https://scholar.google.com/scholar?hl=en&q=neopronouns=academic papers} You can also read some {https://scholar.google.com/scholar?hl=en&q=neopronouns=academic papers}
@ -637,4 +637,6 @@ flags:
Trixic: 'Trixisch' Trixic: 'Trixisch'
Two_Spirit: 'Bispiritueel' Two_Spirit: 'Bispiritueel'
Xenogender: 'Xenogender' Xenogender: 'Xenogender'
link: 'Link' # TODO
full: 'Full calendar' # TODO

View File

@ -1319,9 +1319,10 @@ calendar:
suicide_prevention_day: 'Światowy Dzień Zapobiegania Samobójstwom' suicide_prevention_day: 'Światowy Dzień Zapobiegania Samobójstwom'
suicide_prevention_month: 'Miesiąc Zapobiegania Samobójstwom' suicide_prevention_month: 'Miesiąc Zapobiegania Samobójstwom'
aids_awareness_month: 'Miesiąc Świadomości nt. AIDS' aids_awareness_month: 'Miesiąc Świadomości nt. AIDS'
banner: 'Dziś w kalendarzu' banner: 'Dziś w kalendarzu'
image: image:
header: 'Ściągnij w formie obrazka' header: 'Ściągnij w formie obrazka'
overview: 'Przegląd' overview: 'Przegląd'
labels: 'Etykietki' labels: 'Etykietki'
link: 'Link'
full: 'Pełen kalendarz'

View File

@ -752,3 +752,5 @@ calendar:
header: 'Baixar uma imagem' header: 'Baixar uma imagem'
overview: 'Visão geral' overview: 'Visão geral'
labels: 'Etiquetas' labels: 'Etiquetas'
link: 'Link' # TODO
full: 'Full calendar' # TODO

View File

@ -275,6 +275,8 @@ export default {
if (config.calendar && config.calendar.enabled) { if (config.calendar && config.calendar.enabled) {
routes.push({ path: '/' + config.calendar.route, component: resolve(__dirname, 'routes/calendar.vue') }); routes.push({ path: '/' + config.calendar.route, component: resolve(__dirname, 'routes/calendar.vue') });
routes.push({ path: '/' + config.calendar.route + '/:year(\\d\\d\\d\\d)', component: resolve(__dirname, 'routes/calendar.vue') });
routes.push({ path: '/' + config.calendar.route + '/:year(\\d\\d\\d\\d)-:month(\\d\\d)-:day(\\d\\d)', component: resolve(__dirname, 'routes/calendarDay.vue') });
routes.push({ path: '/calendar-wide', component: resolve(__dirname, 'routes/calendarWide.vue') }); routes.push({ path: '/calendar-wide', component: resolve(__dirname, 'routes/calendarWide.vue') });
} }

View File

@ -1,51 +1,22 @@
<template> <template>
<div> <div v-if="year">
<CommunityNav/> <CommunityNav/>
<h2> <h2>
<Icon v="calendar-star"/> <Icon v="calendar-star"/>
<T>calendar.headerLong</T> ({{year}}) <T>calendar.headerLong</T> <small class="text-muted">({{year.year}})</small>
</h2> </h2>
<CalendarBanner/> <CalendarBanner v-if="year.isCurrent()"/>
<section class="row"> <section class="row">
<div v-for="i in 12" class="col-12 col-sm-6 col-lg-4 py-3"> <div v-for="i in 12" class="col-12 col-sm-6 col-lg-4 py-3">
<h3 class="text-center"><T>calendar.months.{{i}}</T></h3> <h3 class="text-center"><T>calendar.months.{{i}}</T></h3>
<Calendar :year="year" :month="i" markToday/> <Calendar :year="year" :month="i" :mark="today" tooltips/>
</div> </div>
</section> </section>
<section> <CalendarExtra/>
<div class="alert alert-info row">
<div class="col-12 col-lg-6">
<p class="mb-0">
Twitter Bot:
</p>
<p class="mb-0">
<a href="https://twitter.com/CalendarQueer" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="twitter" set="b"/>
@CalendarQueer
</a>
</p>
</div>
<div class="col-12 col-lg-6">
<p class="mb-0">
<T>calendar.image.header</T>:
</p>
<p class="mb-0">
<a href="/calendar/overview.png" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="image"/>
<T>calendar.image.overview</T>
</a>
<a href="/calendar/labels.png" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<Icon v="image"/>
<T>calendar.image.labels</T>
</a>
</p>
</div>
</div>
</section>
<Support/> <Support/>
@ -53,15 +24,21 @@
<Share :title="$t('calendar.header')"/> <Share :title="$t('calendar.header')"/>
</section> </section>
</div> </div>
<NotFound v-else/>
</template> </template>
<script> <script>
import { head } from "../src/helpers"; import { head } from "../src/helpers";
import { calendar } from '../src/calendar/calendar';
import { Day } from '../src/calendar/helpers';
export default { export default {
data() { data() {
return { return {
year: new Date().getFullYear(), year: this.$route.params.year
? calendar.getYear(this.$route.params.year)
: calendar.getCurrentYear(),
today: Day.today(),
} }
}, },
head() { head() {

70
routes/calendarDay.vue Normal file
View File

@ -0,0 +1,70 @@
<template>
<div v-if="year.eventsByDate[day.toString()]">
<CommunityNav/>
<h2>
<Icon v="calendar-star"/>
<T>calendar.headerLong</T> <small class="text-muted">({{day}})</small>
</h2>
<section>
<div class="d-flex justify-content-evenly flex-column-reverse flex-md-row align-items-center align-items-md-start">
<div class="calendar-month my-3">
<h3 class="text-center"><T>calendar.months.{{day.month}}</T></h3>
<Calendar :year="year" :month="day.month" :mark="day"/>
</div>
<div class="calendar-events my-3">
<h3><T :params="{day: day.day}">calendar.dates.{{day.month}}</T> {{day.year}}</h3>
<ul class="list-unstyled mb-0">
<CalendarEvent v-for="event in year.eventsByDate[day.toString()]" :event="event" :key="event.name"/>
</ul>
</div>
</div>
</section>
<CalendarExtra :day="day"/>
<Support/>
<section>
<Share :title="$t('calendar.header')"/>
</section>
</div>
<NotFound v-else/>
</template>
<script>
import { head } from "../src/helpers";
import { calendar } from '../src/calendar/calendar';
import { Day } from '../src/calendar/helpers';
export default {
data() {
const day = new Day(
this.$route.params.year,
this.$route.params.month,
this.$route.params.day,
);
return {
day,
year: calendar.getYear(day.year),
}
},
head() {
return head({
title: this.$t('calendar.headerLong'),
banner: `calendar/overview.png`,
});
},
};
</script>
<style lang="scss" scoped>
.calendar-month {
width: min(18rem, 100%);
}
.calendar-events {
width: min(20rem, 100%);
}
</style>

View File

@ -1,9 +1,9 @@
<template> <template>
<div> <div v-if="year">
<h2 class="d-flex justify-content-between"> <h2 class="d-flex justify-content-between">
<span> <span>
<Icon v="calendar-star"/> <Icon v="calendar-star"/>
<T>calendar.headerLong</T> ({{year}}) <T>calendar.headerLong</T> <small class="text-muted">({{year.year}})</small>
</span> </span>
<span class="h4 mt-2"> <span class="h4 mt-2">
<nuxt-link to="/"> <nuxt-link to="/">
@ -21,16 +21,20 @@
</div> </div>
</section> </section>
</div> </div>
<NotFound v-else/>
</template> </template>
<script> <script>
import { head } from "../src/helpers"; import { head } from "../src/helpers";
import {calendar} from "@/src/calendar/calendar";
export default { export default {
layout: 'basic', layout: 'basic',
data() { data() {
return { return {
year: new Date().getFullYear(), year: this.$route.params.year
? calendar.getYear(this.$route.params.year)
: calendar.getCurrentYear(),
labels: this.$route.query.labels === 'true', labels: this.$route.query.labels === 'true',
} }
}, },

View File

@ -2,7 +2,7 @@ require('../src/dotenv')();
const Twitter = require('twitter'); const Twitter = require('twitter');
const Suml = require('suml'); const Suml = require('suml');
const fs = require('fs'); const fs = require('fs');
const { currentYear } = require('../src/calendar/calendar'); const { calendar } = require('../src/calendar/calendar');
const { Day } = require('../src/calendar/helpers'); const { Day } = require('../src/calendar/helpers');
const locales = require('../src/locales'); const locales = require('../src/locales');
@ -27,7 +27,7 @@ const getEventName = (name) => {
(async () => { (async () => {
const day = Day.today(); const day = Day.today();
const events = currentYear.eventsByDate[day.toString()]; const events = calendar.getCurrentYear().eventsByDate[day.toString()];
console.log(events); console.log(events);
if (events === undefined || events.length === 0) { if (events === undefined || events.length === 0) {
@ -45,7 +45,7 @@ const getEventName = (name) => {
for (let event of events) { for (let event of events) {
tweet += ` - ${getEventName(event.name)}\n`; tweet += ` - ${getEventName(event.name)}\n`;
} }
tweet += `\n${domain}/${config.calendar.route}`; tweet += `\n${domain}/${config.calendar.route}/${day}`;
console.log('------------'); console.log('------------');
console.log(tweet); console.log(tweet);

View File

@ -1,4 +1,4 @@
const { Day, Calendar, Event, EventLevel, day } = require('./helpers'); const { Calendar, Event, EventLevel, day } = require('./helpers');
const internationalEvents = require('../../locale/_/events'); const internationalEvents = require('../../locale/_/events');
const localEvents = require('../../data/events'); const localEvents = require('../../data/events');
@ -13,7 +13,8 @@ for (let name in rawNamedays) {
} }
} }
module.exports.currentYear = new Calendar( module.exports.calendar = new Calendar(
Day.today().year,
[...internationalEvents, ...localEvents], // TODO , ...namedays [...internationalEvents, ...localEvents], // TODO , ...namedays
2021,
2021,
); );

View File

@ -1,9 +1,9 @@
class Day { class Day {
constructor(year, month, day, dayOfWeek) { constructor(year, month, day, dayOfWeek) {
this.year = year; this.year = parseInt(year);
this.month = month; this.month = parseInt(month);
this.day = day; this.day = parseInt(day);
this.dayOfWeek = dayOfWeek || new Date(year, month - 1, day).getDay() || 7; this.dayOfWeek = dayOfWeek ? parseInt(dayOfWeek) : new Date(this.year, this.month - 1, this.day).getDay() || 7;
} }
static fromDate(date) { static fromDate(date) {
@ -121,7 +121,7 @@ module.exports.dayYear = function (day, year) {
return internal; return internal;
} }
module.exports.Calendar = class { class Year {
constructor(year, events) { constructor(year, events) {
this.year = year; this.year = year;
this.events = events; this.events = events;
@ -139,4 +139,35 @@ module.exports.Calendar = class {
this.eventsByDate[date].sort((a, b) => b.level - a.level); this.eventsByDate[date].sort((a, b) => b.level - a.level);
} }
} }
isCurrent() {
return this.year === Day.today().year;
}
}
module.exports.Year = Year;
module.exports.Calendar = class {
constructor(events, minYear = 0, maxYear = 9999) {
this._events = events;
this._minYear = minYear;
this._maxYear = maxYear;
this._years = {};
}
getYear(year) {
year = parseInt(year);
if (year < this._minYear || year > this._maxYear) {
return null;
}
if (this._years[year] === undefined) {
this._years[year] = new Year(year, this._events);
}
return this._years[year];
}
getCurrentYear() {
return this.getYear(Day.today().year);
}
} }