#246 Calendar of queer dates

This commit is contained in:
Avris 2021-08-14 10:42:49 +02:00
parent 262118db41
commit fc6ca5328e
13 changed files with 585 additions and 1 deletions

163
components/Calendar.vue Normal file
View File

@ -0,0 +1,163 @@
<template>
<div class="calendar">
<div v-for="i in (startingDayOfWeek - 1)"></div>
<div v-for="d in iterateMonth(year, month)"
:class="['rounded-circle', getDayClass(d), d.equals(today) ? 'day-today' : '', d.equals(selectedDay) ? 'day-selected' : '']"
@click.stop="selectDay(d)"
:data-flag="getDayFlag(d)"
:style="getDayFlag(d) ? `background-image: url('${getDayFlag(d)}')` : ''"
>
<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 class="card-header">
<strong><T :params="{day: d.day}">calendar.dates.{{d.month}}</T></strong>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li v-for="event in currentYear.eventsByDate[d.toString()]">
<Flag v-if="event.flag" name="" alt="" :img="`/flags/${event.flag}.png`"/>
<Icon v-else v="arrow-circle-right"/>
<T>calendar.events.{{ event.name }}</T>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { Day, iterateMonth, currentYear } from '../src/calendar';
export default {
props: {
year: { required: true },
month: { required: true },
},
data() {
return {
iterateMonth,
currentYear,
today: Day.today(),
selectedDay: null,
}
},
mounted() {
this.$eventHub.$on('calendar-select', selectedDay => {
if (this.selectedDay && !this.selectedDay.equals(selectedDay)) {
this.selectedDay = null;
}
});
},
created() {
if (process.client) {
document.addEventListener('click', this.documentClicked);
}
},
destroyed() {
if (process.client) {
document.removeEventListener('click', this.documentClicked);
}
},
computed: {
startingDayOfWeek() {
return new Day(this.year, this.month, 1).dayOfWeek;
}
},
methods: {
getDayClass(d) {
if (this.currentYear.eventsByDate[d.toString()] === undefined) {
return 'day';
}
if (this.currentYear.eventsByDate[d.toString()].some(e => e.length() === 1)) {
return 'day day-event day-event-single';
}
return 'day day-event day-event-multi';
},
getDayFlag(d) {
for (let event of (this.currentYear.eventsByDate[d.toString()] || []).filter(e => e.length() === 1)) {
return `/flags/${event.flag}.png`;
}
return null;
},
documentClicked() {
if (this.selectedDay) {
this.selectedDay = null;
}
},
selectDay(d) {
if (d.equals(this.selectedDay)) {
this.selectedDay = null;
} else {
this.selectedDay = d;
}
this.$eventHub.$emit('calendar-select', this.selectedDay);
},
},
}
</script>
<style lang="scss" scoped>
@import "assets/style";
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-column-gap: 2px;
grid-row-gap: 2px;
> .day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: space-evenly;
cursor: default;
user-select: none;
position: relative;
&.day-event {
cursor: pointer;
border: 1px solid $primary;
&.day-event-single {
background-color: $primary;
color: $white;
.day-number {
font-weight: bold;
}
&[data-flag] {
background-repeat: no-repeat;
background-position: center;
background-size: cover;
.day-number {
text-shadow: black 1px 1px 3px
}
}
}
&:hover, &.day-selected {
background: lighten($primary, 25%) !important;
@extend .shadow;
.day-number {
color: $white;
}
}
}
&.day-today {
border: 3px solid $black;
@extend .shadow;
}
.day-tooltip {
position: absolute;
bottom: 0;
left: 100%;
width: 300px;
@include media-breakpoint-down('md', $grid-breakpoints) {
position: fixed;
left: 0;
width: 100%;
}
z-index: 999;
cursor: default;
}
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<section v-if="config.calendar && config.calendar.enabled && events !== undefined" class="alert alert-info">
<p class="h3">
<Icon v="calendar-star"/>
<T>calendar.banner</T>:
</p>
<ul class="list-unstyled my-3 ms-3">
<li v-for="event in events">
<Flag v-if="event.flag" name="" alt="" :img="`/flags/${event.flag}.png`"/>
<Icon v-else v="arrow-circle-right"/>
<T>calendar.events.{{ event.name }}</T>
</li>
</ul>
<nuxt-link v-if="link" :to="`/${config.calendar.route}`" class="small">
<Icon v="angle-right"/>
<T>calendar.headerLong</T>
</nuxt-link>
</section>
</template>
<script>
import { Day, currentYear } from '../src/calendar';
export default {
props: {
link: { type: Boolean },
},
data() {
return {
events: currentYear.eventsByDate[Day.today().toString()],
}
}
}
</script>

View File

@ -207,6 +207,7 @@
'/' + this.config.links.mediaRoute,
this.config.links.split ? '/' + this.config.faq.route : '',
'/' + this.config.people.route,
this.config.calendar ? '/' + this.config.calendar.route : '',
],
});
}

View File

@ -38,6 +38,9 @@
if (this.config.faq.enabled) {
links.push({name: 'faq.header', route: this.config.faq.route, icon: 'map-marker-question'});
}
if (this.config.calendar && this.config.calendar.enabled) {
links.push({name: 'calendar.header', route: this.config.calendar.route, icon: 'calendar-star'});
}
if (this.config.people.enabled) {
links.push({name: 'people.header', route: this.config.people.route, icon: 'user-friends'});
}

View File

@ -259,6 +259,10 @@ profile:
flags:
defaultPronoun: 'they'
calendar:
enabled: false
route: 'calendar'
census:
enabled: false

View File

@ -634,3 +634,61 @@ report:
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
calendar:
header: 'Calendar'
headerLong: 'Queer Calendar'
months:
1: 'January'
2: 'February'
3: 'March'
4: 'April'
5: 'May'
6: 'June'
7: 'July'
8: 'August'
9: 'September'
10: 'October'
11: 'November'
12: 'December'
dates:
1: 'January, %day%'
2: 'February, %day%'
3: 'March, %day%'
4: 'April, %day%'
5: 'May, %day%'
6: 'June, %day%'
7: 'July, %day%'
8: 'August, %day%'
9: 'September, %day%'
10: 'October, %day%'
11: 'November, %day%'
12: 'December, %day%'
events:
pride_month: 'Pride Month'
trans_month: 'Trans Awareness Month'
zaimki_birthday: 'Birthday of Pronouns.page'
agender_day: 'Agender Pride Day '
asexuality_day: 'International Asexuality Day'
bisexuality_day: 'Celebrate Bisexuality Day'
drag_day: 'Drag Day'
idahobit: 'International Day Against Homophobia, Transphobia and Biphobia'
intersex_day: 'Intersex Awareness Day'
intersex_remembrance_day: 'Intersex Day of Remembrance'
lesbian_day: 'Lesbian Day'
lesbian_visibility_day: 'Lesbian Visibility Day'
coming_out_day: 'National Coming Out Day'
nonbinary_day: 'Nonbinary People''s Day'
pan_day: 'Pansexual & Panromantic Awareness Day'
trans_remembrance_day: 'Transgender Day of Remembrance'
trans_visibility_day: 'Trans Day of Visibility'
zero_discrimination_day: 'Zero Discrimination Day'
arospec_week: 'Aromantic Spectrum Awareness Week'
asexual_week: 'Asexual Awareness Week'
bisexual_week: 'Bisexual Awareness Week'
pronouns_day: 'Pronouns Day'
trans_week: 'Trans Awareness Week'
trans_parent_day: 'Trans Parent Day'
nonbinary_week: 'Nonbinary Awareness Week'
polyamory_day: 'Polyamory Day'
banner: 'We''re celebrating'

View File

@ -994,6 +994,10 @@ profile:
flags:
defaultPronoun: 'on_'
calendar:
enabled: true
route: 'kalendarz'
census:
enabled: true
route: 'spis'

View File

@ -1261,3 +1261,61 @@ flags:
Trixic: 'Triksyjs{adjective_n_k}'
Two_Spirit: 'Dwie dusze'
Xenogender: 'Ksenopłciow{adjective_n}'
calendar:
header: 'Kalendarz'
headerLong: 'Queerowy Kalendarz'
months:
1: 'Styczeń'
2: 'Luty'
3: 'Marzec'
4: 'Kwiecień'
5: 'Maj'
6: 'Czerwiec'
7: 'Lipiec'
8: 'Sierpień'
9: 'Wrzesień'
10: 'Październik'
11: 'Listopad'
12: 'Grudzień'
dates:
1: '%day% stycznia'
2: '%day% lutego'
3: '%day% marca'
4: '%day% kwietnia'
5: '%day% maja'
6: '%day% czerwca'
7: '%day% lipca'
8: '%day% sierpnia'
9: '%day% września'
10: '%day% października'
11: '%day% listopada'
12: '%day% grudnia'
events:
pride_month: 'Miesiąc {/slowniki/terminologia#pride=Dumy}'
trans_month: 'Miesiąc Świadomości nt. {/slowniki/terminologia#transpłciowość=Transpłciowości}'
zaimki_birthday: 'Urodziny zaimki.pl'
agender_day: 'Dzień Osób {/slowniki/terminologia#apłciowość=Apłciowych}'
asexuality_day: 'Światowy Dzień {/slowniki/terminologia#aseksualność=Aseksualności}'
bisexuality_day: 'Dzień {/slowniki/terminologia#biseksualność=Biseksualności}'
drag_day: 'Dzień {/slowniki/terminologia#drag=Dragu}'
idahobit: 'Międzynarodowy Dzień Przeciw {/slowniki/terminologia#homofobia=Homofobii}, {/slowniki/terminologia#transfobia=Transfobii} i {/slowniki/terminologia#bifobia=Bifobii}'
intersex_day: 'Dzień Świadomości nt. {/slowniki/terminologia#homofobia=Interpłciowości}'
intersex_remembrance_day: 'Dzień Pamięci Osób {/slowniki/terminologia#interpłciowość=Interpłciowych}'
lesbian_day: 'Dzień {/slowniki/terminologia#lesbijka=Lesbijek}'
lesbian_visibility_day: 'Dzień Widoczności {/slowniki/terminologia#lesbijka=Lesbijek}'
coming_out_day: 'Dzień {/slowniki/terminologia#coming%20out=Wychodzenia z Szafy}'
nonbinary_day: 'Dzień Osób {/slowniki/terminologia#niebinarność=Niebinarnych}'
pan_day: 'Dzień Świadomości nt. {/slowniki/terminologia#panseksualność=Panseksualności} i {/slowniki/terminologia#panromantyczność=Panromantyczności}'
trans_remembrance_day: 'Dzień Pamięci Osób {/slowniki/terminologia#transpłciowość=Transpłciowych}'
trans_visibility_day: 'Dzień Widoczności Osób {/slowniki/terminologia#transpłciowość=Transpłciowych}'
zero_discrimination_day: 'Zero Discrimination Day'
arospec_week: 'Tydzień Świadomości nt. Spektrum {/slowniki/terminologia#aromantyczność=Aromantyczności}'
asexual_week: 'Tydzień Świadomości nt. {/slowniki/terminologia#aseksualność=Aseksualności}'
bisexual_week: 'Tydzień Świadomości nt. {/slowniki/terminologia#biseksualność=Biseksualności}'
pronouns_day: 'Dzień {https://avris.it/blog/czemu-ka%C5%BCdy-powinien-mie%C4%87-zaimki-w-bio=Zaimków}'
trans_week: 'Tydzień Świadomości nt. {/slowniki/terminologia#transpłciowość=Transpłciowości}'
trans_parent_day: 'Dzień Rodzicielstwa Osób {/slowniki/terminologia#transpłciowość=Transpłciowych}'
nonbinary_week: 'Tydzień Świadomości nt. {/slowniki/terminologia#niebinarność=Niebinarności Płciowej}'
polyamory_day: 'Dzień {/slowniki/terminologia#poliamoria=Poliamorii}'
banner: 'Obchodzimy właśnie'

View File

@ -245,6 +245,10 @@ export default {
}
}
if (config.calendar && config.calendar.enabled) {
routes.push({ path: '/' + config.calendar.route, component: resolve(__dirname, 'routes/calendar.vue') });
}
if (config.api !== null) {
routes.push({ path: '/api', component: resolve(__dirname, 'routes/api.vue') });
}

41
routes/calendar.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div>
<LinksNav/>
<h2>
<Icon v="calendar-star"/>
<T>calendar.headerLong</T> ({{year}})
</h2>
<CalendarBanner/>
<section class="row">
<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>
<Calendar :year="year" :month="i"/>
</div>
</section>
<Support/>
<section>
<Share :title="$t('calendar.header')"/>
</section>
</div>
</template>
<script>
import { head } from "../src/helpers";
export default {
data() {
return {
year: new Date().getFullYear(),
}
},
head() {
return head({
title: this.$t('calendar.headerLong'),
});
},
};
</script>

View File

@ -19,6 +19,8 @@
</div>
</section>
<CalendarBanner link/>
<section>
<Share/>
</section>
@ -44,7 +46,7 @@
</template>
<script>
import {mapState} from "vuex";
import { mapState } from "vuex";
export default {
computed: {

212
src/calendar.js Normal file
View File

@ -0,0 +1,212 @@
export class Day {
constructor(year, month, day, dayOfWeek) {
this.year = year;
this.month = month;
this.day = day;
this.dayOfWeek = dayOfWeek || new Date(year, month - 1, day).getDay() || 7;
}
static fromDate(date) {
return new Day(date.getFullYear(), date.getMonth() + 1, date.getDate());
}
static today() {
return Day.fromDate(new Date);
}
equals(other) {
return other && this.year === other.year && this.month === other.month && this.day === other.day;
}
toString() {
return `${this.year}-${this.month.toString().padStart(2, '0')}-${this.day.toString().padStart(2, '0')}`;
}
}
export function* iterateMonth(year, month) {
for (let day = 1; day <= 31; day++) {
let d = new Date(year, month - 1, day);
if (d.getDate() !== day) { return; }
yield new Day(year, month, day, d.getDay() || 7);
}
}
export class Event {
constructor(name, flag, month, generator) {
this.name = name;
this.flag = flag;
this.month = month;
this.generator = generator;
this.daysMemoise = {}
}
getDays(year) {
if (this.daysMemoise[year] === undefined) {
this.daysMemoise[year] = [...this.generator(iterateMonth(year, this.month))];
}
return this.daysMemoise[year];
}
length() {
return [...this.getDays(2021)].length;
}
}
function day (dayOfMonth) {
function *internal (monthDays) {
for (let d of monthDays) {
if (d.day === dayOfMonth) {
yield d;
}
}
}
return internal;
}
function *month (monthDays) {
for (let d of monthDays) {
yield d;
}
}
function week (generator) {
function *internal (monthDays) {
let count = 0;
for (let d of generator(monthDays)) {
yield d;
count++;
if (count === 7) {
return;
}
}
}
return internal;
}
export const events = [
new Event('zaimki_birthday', null, 8, day(14)), // TODO
new Event('pride_month', 'Progress Pride', 6, month),
new Event('trans_month', 'Transgender', 11, month),
new Event('zaimki_birthday', null, 7, day(23)),
new Event('agender_day', 'Agender', 5, day(19)),
new Event('asexuality_day', 'Asexual', 4, day(6)),
new Event('bisexuality_day', 'Bisexual', 9, day(23)),
new Event('drag_day', '-Drag', 6, day(16)),
new Event('idahobit', null, 5, day(17)),
new Event('intersex_day', 'Intersex', 10, day(26)),
new Event('intersex_remembrance_day', 'Intersex', 11, day(8)),
new Event('lesbian_day', 'Lesbian', 10, day(8)),
new Event('lesbian_visibility_day', 'Lesbian', 4, day(26)),
new Event('coming_out_day', null, 10, day(11)),
new Event('nonbinary_day', 'Nonbinary', 7, day(14)),
new Event('pan_day', 'Pansexual', 5, day(24)),
new Event('trans_remembrance_day', 'Transgender', 11, day(20)),
new Event('trans_visibility_day', 'Transgender', 3, day(31)),
new Event('zero_discrimination_day', null, 3, day(1)),
new Event('polyamory_day', 'Polyamorous', 11, day(23)),
new Event('arospec_week', 'Aromantic', 2, week(function *(monthDays) {
let started = false;
for (let d of monthDays) {
if (!started && d.day > 14 && d.dayOfWeek === 7) {
started = true;
}
if (started) {
yield d;
}
}
})),
new Event('asexual_week', 'Asexual', 10, week(function *(monthDays) {
let started = false;
for (let d of monthDays) {
if (!started && d.day >= 19 && d.dayOfWeek === 7) {
started = true;
}
if (started) {
yield d;
}
}
})),
new Event('bisexual_week', 'Bisexual', 9, week(function *(monthDays) {
for (let d of monthDays) {
if (d.day >= 16 && d.day <= 22) {
yield d;
}
}
})),
new Event('pronouns_day', null, 10, function *(monthDays) {
let wednesdays = 0;
for (let d of monthDays) {
if (d.dayOfWeek === 3) {
wednesdays++;
if (wednesdays === 3) {
yield d;
return;
}
}
}
}),
new Event('trans_week', 'Transgender', 11, week(function *(monthDays) {
for (let d of monthDays) {
if (d.day >= 13 && d.day <= 19) {
yield d;
}
}
})),
new Event('trans_parent_day', 'Transgender', 9, week(function *(monthDays) {
for (let d of monthDays) {
if (d.dayOfWeek === 7) {
yield d;
return;
}
}
})),
new Event('nonbinary_week', 'Nonbinary', 7, week(function *(monthDays) {
let buffer = [];
for (let d of monthDays) {
if (d.day >= 14) {
for (let dd of buffer) {
yield dd;
}
buffer = [];
yield d;
continue;
}
if (d.dayOfWeek === 1) {
buffer = [];
}
buffer.push(d);
}
})),
];
export class Year {
constructor(year, events) {
this.year = year;
this.events = events;
this.eventsByDate = {};
for (let event of events) {
for (let d of event.getDays(year)) {
const k = d.toString();
if (this.eventsByDate[k] === undefined) { this.eventsByDate[k] = []; }
this.eventsByDate[k].push(event);
}
}
}
}
export const currentYear = new Year(new Date().getFullYear(), events);

BIN
static/flags/-Drag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB