mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-26 22:43:06 -04:00
199 lines
6.1 KiB
Vue
199 lines
6.1 KiB
Vue
<template>
|
|
<Tooltip v-if="celebrate1M" :text="$t('home.million')">
|
|
<span
|
|
ref="confettiLogo"
|
|
:class="['bg-primary logo-wrapper rounded-circle d-inline-flex justify-content-center align-items-center', $attrs.class]"
|
|
@mouseenter="fireConfetti"
|
|
>
|
|
<span class="logo" v-html="svg.replace(`<path `, `<path style='fill: #fff' `)"></span>
|
|
</span>
|
|
</Tooltip>
|
|
<span
|
|
v-else-if="flag"
|
|
:class="['logo-wrapper rounded-circle d-inline-flex justify-content-center align-items-center', forceShowFlag || forceShowFlagDyn ? 'logo-flag-forced' : '', flagName ? 'logo-has-flag' : '', $attrs.class]"
|
|
:style="flagName ? `--flag: url('/flags/${flagName}.png')` : ''"
|
|
@transitionend="resetFlagIfNotOverwritten"
|
|
>
|
|
<span class="logo" v-html="svg"></span>
|
|
</span>
|
|
<span v-else :class="['logo', $attrs.class]" v-html="svg"></span>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { useFetch } from 'nuxt/app';
|
|
import { storeToRefs } from 'pinia';
|
|
import { defineComponent } from 'vue';
|
|
|
|
import logoSvg from '../public/logo/logo.svg?raw';
|
|
import { buildCalendar } from '../src/calendar/calendar.ts';
|
|
import { Day } from '../src/calendar/helpers.ts';
|
|
import { ImmutableArray } from '../src/helpers.ts';
|
|
import { useMainStore } from '../store/index.ts';
|
|
|
|
interface Data {
|
|
svg: string;
|
|
flagName: string | null;
|
|
forceShowFlagDyn: boolean;
|
|
}
|
|
|
|
interface Refs {
|
|
confettiLogo: HTMLSpanElement | undefined;
|
|
}
|
|
|
|
export default defineComponent({
|
|
props: {
|
|
flag: { type: Boolean },
|
|
forceShowFlag: { type: Boolean },
|
|
day: { default: () => Day.today(), type: Day },
|
|
},
|
|
async setup() {
|
|
const runtimeConfig = useRuntimeConfig();
|
|
|
|
const { data: stats } = await useFetch<{ overall: { users: number } }>(
|
|
'/api/admin/stats-public',
|
|
{ lazy: true },
|
|
);
|
|
|
|
return {
|
|
stats,
|
|
selectedDay: storeToRefs(useMainStore()).selectedDay,
|
|
calendar: buildCalendar(runtimeConfig.public.baseUrl),
|
|
};
|
|
},
|
|
data(): Data {
|
|
return {
|
|
svg: logoSvg.replace('/></svg>', 'fill="currentColor"/></svg>'),
|
|
flagName: null,
|
|
forceShowFlagDyn: false,
|
|
};
|
|
},
|
|
computed: {
|
|
$tRefs(): Refs {
|
|
return this.$refs as unknown as Refs;
|
|
},
|
|
celebrate1M(): boolean {
|
|
return this.stats !== null && this.stats.overall.users >= 1_000_000 && this.stats.overall.users < 1_005_000;
|
|
},
|
|
},
|
|
watch: {
|
|
selectedDay() {
|
|
this.forceShowFlagDyn = !!this.selectedDay;
|
|
// removing the flag from the selected day is deferred until the transition has finished
|
|
// so that it does not suddenly change
|
|
if (this.selectedDay !== null) {
|
|
this.flagName = this.selectFlag();
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
setTimeout(() => this.fireConfetti(), 1000);
|
|
},
|
|
methods: {
|
|
async fireConfetti() {
|
|
await this.$nextTick();
|
|
if (!this.$tRefs.confettiLogo?.offsetParent) {
|
|
return;
|
|
}
|
|
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
return;
|
|
}
|
|
|
|
const confetti = (await import('canvas-confetti')).default;
|
|
|
|
const origin = {
|
|
x: (this.$tRefs.confettiLogo.offsetLeft + this.$tRefs.confettiLogo.offsetWidth) / window.innerWidth,
|
|
y: (this.$tRefs.confettiLogo.offsetTop + this.$tRefs.confettiLogo.offsetHeight) / window.innerHeight,
|
|
};
|
|
|
|
const centre = { x: 0.5, y: 0.5 };
|
|
|
|
const angleRadians = -Math.atan((centre.y - origin.y) / (centre.x - origin.x));
|
|
const angleDegrees = angleRadians * (180 / Math.PI);
|
|
|
|
await confetti({
|
|
particleCount: 200,
|
|
angle: angleDegrees,
|
|
spread: 120,
|
|
shapes: ['star'],
|
|
origin,
|
|
scalar: 0.8,
|
|
decay: 0.9,
|
|
});
|
|
},
|
|
selectFlag(): string | null {
|
|
const events = this.calendar.getCurrentYear()!.eventsByDate[(this.selectedDay || this.day).toString()];
|
|
if (!events) {
|
|
return null;
|
|
}
|
|
return new ImmutableArray(...events)
|
|
.filter((e) => e.display.type === 'flag' && !e.display.name.startsWith('_'))
|
|
.sorted((a, b) => b.level - a.level)
|
|
.groupBy((e) => e.level)
|
|
.indexOrFallback(0, ['0', new ImmutableArray()])[1]
|
|
.map((e) => e.display.name)
|
|
.randomElement();
|
|
},
|
|
resetFlagIfNotOverwritten(): void {
|
|
if (this.selectedDay === null) {
|
|
this.flagName = this.selectFlag();
|
|
}
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.logo-wrapper {
|
|
width: 1.3em;
|
|
height: 1.3em;
|
|
position: relative;
|
|
overflow: hidden;
|
|
&:before {
|
|
content: ' ';
|
|
display: block;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-image: var(--flag);
|
|
background-position: center;
|
|
background-size: cover;
|
|
background-repeat: no-repeat;
|
|
z-index: -5;
|
|
|
|
opacity: 0;
|
|
transition: all .25s ease-in-out;
|
|
}
|
|
}
|
|
|
|
.logo {
|
|
height: 1em;
|
|
width: 1em;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
svg {
|
|
vertical-align: baseline !important;
|
|
}
|
|
}
|
|
|
|
.logo-wrapper.logo-flag-forced.logo-has-flag, a:hover .logo-wrapper.logo-has-flag {
|
|
svg path {
|
|
stroke: white;
|
|
stroke-width: 10;
|
|
}
|
|
&:before {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
body[data-theme="dark"] {
|
|
.logo-wrapper.logo-flag-forced.logo-has-flag, a:hover .logo-wrapper.logo-has-flag {
|
|
svg path {
|
|
stroke: black;
|
|
}
|
|
}
|
|
}
|
|
</style>
|