Skip to content

Commit 7f31cf9

Browse files
committed
Add LandingMotd component and sample MOTDs to the database
1 parent 813694a commit 7f31cf9

File tree

3 files changed

+281
-9
lines changed

3 files changed

+281
-9
lines changed

app/components/Landing/LandingHero.vue

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import LandingHeroActions from '@/components/Landing/LandingHeroActions.vue'
44
import LandingHeroGlobe from '@/components/Landing/LandingHeroGlobe.vue'
55
import LandingHeroShader from '@/components/Landing/LandingHeroShader.vue'
66
import LandingHeroStats from '@/components/Landing/LandingHeroStats.vue'
7+
import LandingMotd from '@/components/Landing/LandingMotd.vue'
78
89
interface CommunityStats {
910
members: number
@@ -32,9 +33,7 @@ defineProps<{
3233
<h1 class="hero-overlay__title">
3334
HIVECOM
3435
</h1>
35-
<p class="hero-overlay__tagline">
36-
A community of friends from all around the world
37-
</p>
36+
<LandingMotd fallback-text="A community of friends from all around the world" />
3837
</div>
3938

4039
<LandingHeroStats class="hero-overlay__stats" :community-stats="communityStats" :loading="loading" />
@@ -155,12 +154,6 @@ defineProps<{
155154
}
156155
}
157156
158-
.hero-overlay__tagline {
159-
font-size: var(--font-size-l);
160-
margin: 0;
161-
opacity: 0.82;
162-
}
163-
164157
.hero-overlay__stats {
165158
margin-top: 0.5rem;
166159
width: 100%;
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<script setup lang="ts">
2+
import { onBeforeUnmount, onMounted, ref } from 'vue'
3+
4+
const props = withDefaults(defineProps<{
5+
fallbackText: string
6+
batchSize?: number
7+
}>(), {
8+
batchSize: 100,
9+
})
10+
11+
const supabase = useSupabaseClient()
12+
13+
const displayText = ref(props.fallbackText)
14+
const phase = ref<'idle' | 'out' | 'in'>('idle')
15+
const renderKey = ref(0)
16+
const motdPool = ref<string[]>([])
17+
const totalAvailable = ref<number | null>(null)
18+
const isLoading = ref(false)
19+
const fetchingBatch = ref(false)
20+
21+
const LETTER_STAGGER_MS = 18
22+
const LETTER_OUT_MS = 220
23+
const LETTER_IN_MS = 240
24+
const AUTO_SWITCH_MS = 60_000
25+
26+
let autoTimer: ReturnType<typeof setTimeout> | undefined
27+
28+
function scheduleAutoSwitch() {
29+
if (autoTimer)
30+
clearTimeout(autoTimer)
31+
32+
autoTimer = setTimeout(() => {
33+
void advanceMotd()
34+
}, AUTO_SWITCH_MS)
35+
}
36+
37+
function sleep(ms: number) {
38+
return new Promise(resolve => setTimeout(resolve, ms))
39+
}
40+
41+
function prefersReducedMotion(): boolean {
42+
try {
43+
return !!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
44+
}
45+
catch {
46+
return false
47+
}
48+
}
49+
50+
function shuffle(list: string[]): string[] {
51+
const arr = [...list]
52+
for (let i = arr.length - 1; i > 0; i -= 1) {
53+
const j = Math.floor(Math.random() * (i + 1))
54+
const temp = arr[i]!
55+
arr[i] = arr[j]!
56+
arr[j] = temp
57+
}
58+
return arr
59+
}
60+
61+
function getOffset(count: number | null, size: number): number {
62+
if (!count || count <= size)
63+
return 0
64+
return Math.floor(Math.random() * Math.max(1, count - size + 1))
65+
}
66+
67+
async function fetchBatch() {
68+
if (fetchingBatch.value)
69+
return
70+
71+
fetchingBatch.value = true
72+
73+
try {
74+
const offset = getOffset(totalAvailable.value, props.batchSize)
75+
const { data, error, count } = await supabase
76+
.from('motds')
77+
.select('message', { count: 'estimated' })
78+
.order('created_at', { ascending: false })
79+
.range(offset, offset + props.batchSize - 1)
80+
81+
if (error)
82+
throw error
83+
84+
totalAvailable.value = count ?? totalAvailable.value
85+
const messages = (data || []).map(entry => entry.message).filter(Boolean)
86+
motdPool.value = shuffle(messages)
87+
}
88+
catch (error) {
89+
console.error('Failed to fetch MOTDs', error)
90+
}
91+
finally {
92+
fetchingBatch.value = false
93+
}
94+
}
95+
96+
async function advanceMotd() {
97+
if (isLoading.value || phase.value !== 'idle')
98+
return
99+
100+
isLoading.value = true
101+
102+
const reduceMotion = prefersReducedMotion()
103+
if (!reduceMotion)
104+
phase.value = 'out'
105+
106+
const fetchPromise = (async () => {
107+
if (!motdPool.value.length)
108+
await fetchBatch()
109+
110+
const next = motdPool.value.pop()
111+
return next || props.fallbackText
112+
})()
113+
114+
if (!reduceMotion) {
115+
const outTotal = LETTER_OUT_MS + Math.max(0, displayText.value.length - 1) * LETTER_STAGGER_MS
116+
await sleep(outTotal)
117+
}
118+
119+
displayText.value = await fetchPromise
120+
renderKey.value += 1
121+
122+
if (!reduceMotion) {
123+
phase.value = 'in'
124+
const inTotal = LETTER_IN_MS + Math.max(0, displayText.value.length - 1) * LETTER_STAGGER_MS
125+
await sleep(inTotal)
126+
phase.value = 'idle'
127+
}
128+
129+
isLoading.value = false
130+
131+
scheduleAutoSwitch()
132+
}
133+
134+
async function handleClick() {
135+
await advanceMotd()
136+
}
137+
138+
onMounted(() => {
139+
scheduleAutoSwitch()
140+
})
141+
142+
onBeforeUnmount(() => {
143+
if (autoTimer)
144+
clearTimeout(autoTimer)
145+
})
146+
</script>
147+
148+
<template>
149+
<p
150+
class="hero-motd"
151+
role="button"
152+
tabindex="0"
153+
:aria-busy="isLoading"
154+
:class="{ 'hero-motd--loading': isLoading }"
155+
@click="handleClick"
156+
@keydown.enter.prevent="handleClick"
157+
@keydown.space.prevent="handleClick"
158+
>
159+
<span class="hero-motd__sr">{{ displayText }}</span>
160+
161+
<span
162+
:key="`${renderKey}-${displayText}`"
163+
class="hero-motd__text"
164+
:class="{
165+
'hero-motd__text--out': phase === 'out',
166+
'hero-motd__text--in': phase === 'in',
167+
}"
168+
aria-hidden="true"
169+
>
170+
<span
171+
v-for="(char, index) in Array.from(displayText)"
172+
:key="`${renderKey}-${index}-${char}`"
173+
class="hero-motd__letter"
174+
:style="{
175+
'--i': index,
176+
'--stagger': `${LETTER_STAGGER_MS}ms`,
177+
}"
178+
>{{ char === ' ' ? '\u00A0' : char }}</span>
179+
</span>
180+
</p>
181+
</template>
182+
183+
<style scoped>
184+
.hero-motd {
185+
font-size: var(--font-size-l);
186+
margin: 0;
187+
opacity: 0.82;
188+
cursor: default;
189+
user-select: none;
190+
transition:
191+
opacity var(--transition),
192+
transform var(--transition);
193+
}
194+
195+
.hero-motd__text {
196+
display: inline-block;
197+
min-height: 1.4em;
198+
}
199+
200+
.hero-motd__sr {
201+
position: absolute;
202+
width: 1px;
203+
height: 1px;
204+
padding: 0;
205+
margin: -1px;
206+
overflow: hidden;
207+
clip: rect(0, 0, 0, 0);
208+
white-space: nowrap;
209+
border: 0;
210+
}
211+
212+
.hero-motd__letter {
213+
display: inline-block;
214+
will-change: opacity, transform;
215+
}
216+
217+
.hero-motd__text--out .hero-motd__letter {
218+
animation-name: hero-motd-letter-out;
219+
animation-duration: 220ms;
220+
animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
221+
animation-fill-mode: both;
222+
animation-delay: calc(var(--i) * var(--stagger));
223+
}
224+
225+
.hero-motd__text--in .hero-motd__letter {
226+
animation-name: hero-motd-letter-in;
227+
animation-duration: 240ms;
228+
animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
229+
animation-fill-mode: both;
230+
animation-delay: calc(var(--i) * var(--stagger));
231+
}
232+
233+
.hero-motd--loading {
234+
opacity: 0.82;
235+
transform: none;
236+
}
237+
238+
@keyframes hero-motd-letter-out {
239+
from {
240+
opacity: 1;
241+
transform: translateY(0);
242+
}
243+
to {
244+
opacity: 0;
245+
transform: translateY(4px);
246+
}
247+
}
248+
249+
@keyframes hero-motd-letter-in {
250+
from {
251+
opacity: 0;
252+
transform: translateY(-4px);
253+
}
254+
to {
255+
opacity: 1;
256+
transform: translateY(0);
257+
}
258+
}
259+
260+
@media (prefers-reduced-motion: reduce) {
261+
.hero-motd__letter {
262+
animation: none !important;
263+
}
264+
}
265+
</style>

supabase/seed.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,20 @@ The server is configured for casual play with a friendly, welcoming environment.
487487
Come join us and let''s have some fun together!
488488
', FALSE, ARRAY['cs2', 'gameserver', 'gaming', 'announcement']);
489489

490+
-- Insert sample MOTDs
491+
INSERT INTO public.motds(message, created_at, created_by, modified_at, modified_by)
492+
VALUES
493+
('This is a message of the day.', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
494+
('This is another message of the day.', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
495+
('Jo moin Leude', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
496+
('Message of the day', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
497+
('You''re still here?', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
498+
('Stay awhile and listen', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
499+
('You''re among friends now', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1'),
500+
('Don''t forget to hydrate!', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1', NOW(), '018d224c-0e49-4b6d-b57a-87299605c2b1')
501+
ON CONFLICT
502+
DO NOTHING;
503+
490504
-- Insert test complaints
491505
INSERT INTO public.complaints(created_at, created_by, message, response, responded_by, responded_at, acknowledged, context_user, context_gameserver)
492506
VALUES

0 commit comments

Comments
 (0)