drop/server/components/UserHeader/FriendsWidgetPanel.vue
wdunn001 180241c266
Some checks failed
Server CI / Lint (push) Has been cancelled
Server CI / Typecheck (push) Has been cancelled
community: wire server registry deep links + main nav (friends widget, Community submenu, profile link)
- Resolve gameServerRegistry.json's null deep_link entries against the
  live drop-db catalog (WoW, EverQuest, Among Us match real Game rows;
  OpenDAoC and the Goldberg relay genuinely have none, left null and
  documented why).
- Fix the "0 online now" panel bug: the community landing page's
  persistent-world quick-strip was inferring presence from PlaySession
  name-matching (presenceService.getServerPresence), which hardcodes
  available=true even with zero matches -- always showing a fake "0"
  for EQEmu instead of the honest unavailable state. Switched it to the
  same probe-backed GameServer feed ServerStatusPanel already uses
  (joined by the shared wow/eqemu/daoc slug), so known-zero and
  count-unavailable never render the same.
- Header people icon now opens a friends flyout (list, incoming
  requests with accept/decline, entry into chat with a friend) instead
  of going nowhere, mirroring the existing notifications bell's
  Menu/MenuButton/MenuItems pattern; badge shows pending request count.
  Mobile sidebar links it straight to /community/friends.
- Community gets a hover submenu in the main nav (desktop) / inline
  sublist (mobile) listing Friends, Chat, Devices, Mods, Servers,
  Issues, Achievements.
- User dropdown menu gets a "Your profile" entry resolved from the
  signed-in user's own username (/community/profile/<username>), not
  hardcoded.
2026-08-03 11:02:58 -04:00

144 lines
4.5 KiB
Vue

<template>
<PanelWidget class="flex-col gap-y-2">
<div class="border-b border-zinc-700 pb-3 p-2">
<div
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
>
<div class="ml-4 mt-2">
<h3 class="text-base font-semibold text-zinc-100 text-sm">
{{ $t("community.friends.title") }}
</h3>
</div>
<div class="ml-4 mt-2 shrink-0">
<NuxtLink
to="/community/friends"
type="button"
class="text-sm text-zinc-400"
>
<i18n-t
keypath="community.friends.viewAll"
tag="span"
scope="global"
>
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
</div>
</div>
</div>
<div class="flex flex-col gap-y-3 max-h-[400px] overflow-y-scroll px-2">
<!-- Incoming friend requests -- the "invites" half of the ask, surfaced
right at the entry point instead of only on the full page. -->
<div
v-if="props.friends && props.friends.incomingRequests.length > 0"
class="flex flex-col gap-y-2"
>
<h4 class="text-xs font-semibold uppercase tracking-wide text-zinc-500">
{{ $t("community.friends.incoming") }}
</h4>
<div
v-for="req in props.friends.incomingRequests"
:key="req.id"
class="flex items-center justify-between gap-x-2"
>
<div class="flex items-center gap-x-2 min-w-0">
<UserAvatar
:avatar-url="req.user.avatarUrl"
:name="req.user.displayName"
:size-px="28"
/>
<span class="text-zinc-100 text-sm truncate">{{
req.user.displayName
}}</span>
</div>
<div class="flex gap-x-1.5 shrink-0">
<button
type="button"
class="rounded-md bg-emerald-600 px-2 py-1 text-xs font-semibold text-white hover:bg-emerald-500 duration-200"
@click="accept(req.id)"
>
{{ $t("community.friends.accept") }}
</button>
<button
type="button"
class="rounded-md bg-zinc-700 px-2 py-1 text-xs font-semibold text-zinc-200 hover:bg-zinc-600 duration-200"
@click="decline(req.id)"
>
{{ $t("community.friends.decline") }}
</button>
</div>
</div>
<div class="h-0.5 rounded-full w-full bg-zinc-800 mt-1" />
</div>
<!-- Friends list, with a direct entry into chat with each one. -->
<div
v-for="friend in props.friends?.friends ?? []"
:key="friend.id"
class="flex items-center justify-between gap-x-2"
>
<NuxtLink
:to="`/community/profile/${friend.username}`"
class="flex items-center gap-x-2 min-w-0"
>
<UserAvatar
:avatar-url="friend.avatarUrl"
:name="friend.displayName"
:size-px="28"
/>
<span class="text-zinc-100 text-sm truncate">{{
friend.displayName
}}</span>
</NuxtLink>
<NuxtLink
:to="`/community/chat?dm=${friend.id}`"
class="text-xs text-blue-400 hover:text-blue-300 shrink-0"
>
{{ $t("community.friends.message") }}
</NuxtLink>
</div>
</div>
<div
v-if="
props.friends &&
props.friends.friends.length === 0 &&
props.friends.incomingRequests.length === 0
"
class="text-sm text-zinc-400 p-3 text-center w-full"
>
{{ $t("community.friends.noFriends") }}
</div>
<div
v-else-if="!props.friends"
class="text-sm text-zinc-400 p-3 text-center w-full"
>
{{ $t("common.srLoading") }}
</div>
</PanelWidget>
</template>
<script setup lang="ts">
import type { FriendsListResult } from "~/composables/community-friends";
const props = defineProps<{
friends: FriendsListResult | null;
}>();
async function accept(requestId: string) {
await $dropFetch(`/api/v1/community/friends/requests/${requestId}/accept`, {
method: "POST",
});
await refreshCommunityFriends();
}
async function decline(requestId: string) {
await $dropFetch(`/api/v1/community/friends/requests/${requestId}/decline`, {
method: "POST",
});
await refreshCommunityFriends();
}
</script>