feat: implement a proper navbar

This commit is contained in:
Twig 2025-08-08 02:01:36 +02:00
parent 307969ffe5
commit 28f5e8dc27
Signed by: twig
SSH key fingerprint: SHA256:nBO+OwpTkd8LYhe38PIqdxmDvkIg9Vw2EbrRZM97dkU
4 changed files with 227 additions and 76 deletions

View file

@ -0,0 +1,124 @@
<template>
<div id="navbar">
<div v-for="entry of props.clientItems" id="navbar-left">
<button class="navbar-item" :title="entry.title"
@click.prevent="entry.callback()">
<Icon :name="entry.icon" class="navbar-item-icon" />
</button>
</div>
<div id="navbar-middle">
<NuxtImg v-if="props.contextIcon"
class="context-icon"
:alt="props.contextName"
:src="props.contextIcon" />
<DefaultIcon v-else-if="props.contextName && props.guildUuid"
class="context-icon"
:alt="props.contextName"
:name="props.contextName" :seed="props.guildUuid"/>
<div class="context-title">
{{ props.contextName }}
</div>
</div>
<div v-for="entry of props.channelItems" id="navbar-right">
<button class="navbar-item" :title="entry.title"
@click.prevent="entry.callback()">
<Icon :name="entry.icon" class="navbar-item-icon" />
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { NavbarInterface, NavbarItem } from '~/types/interfaces';
const props = defineProps<NavbarInterface>();
</script>
<style scoped>
#navbar {
--navbar-height: 5dvh;
--navbar-icon-size: 3dvh;
--navbar-gap: calc(3dvh * .2);
--side-margins: calc(.6dvw + .35dvh); /* try to make it reasonable at any aspect ratio */
left: 0;
right: 0;
top: 0;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
width: 100%;
display: flex;
justify-content: center;
background: var(--optional-topbar-background);
background-color: var(--topbar-background-color);
border-bottom: 1px solid var(--padding-color);
}
#navbar-left,
#navbar-middle,
#navbar-right {
top: 0;
height: var(--navbar-height);
display: inline-flex;
justify-content: center;
align-items: center;
gap: var(--navbar-gap);
}
#navbar-left {
left: var(--side-margins);
position: absolute;
}
#navbar-middle {
max-width: 50dvw;
}
#navbar-right {
right: var(--side-margins);
position: absolute;
}
.context-icon {
height: calc(var(--navbar-height) * 0.7);
width: calc(var(--navbar-height) * 0.7);
border-radius: var(--guild-icon-radius);
}
.context-title {
min-height: var(--navbar-height);
max-height: var(--navbar-height);
font-weight: 500;
font-size: calc(var(--navbar-height) * .5);
line-height: calc(var(--navbar-height) * .9);
}
.navbar-item {
color: var(--reply-text-color);
background-color: transparent;
border: none;
cursor: pointer;
padding: 0;
transition: color 300ms;
display: flex;
align-items: center;
height: var(--navbar-icon-size);
}
.navbar-item:hover {
color: var(--primary-highlighted-color);
}
.navbar-item-icon {
width: var(--navbar-icon-size);
height: 100%;
}
</style>

View file

@ -1,13 +1,7 @@
<template> <template>
<Loading v-show="loading" /> <Loading v-show="loading" />
<div :class="{ hidden: loading, visible: !loading }" id="client-root"> <div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar"> <Navbar id="navbar" v-bind="navbar" />
<div class="homebar-item">
<marquee>
gorb!!!!!
</marquee>
</div>
</div>
<div id="page-content"> <div id="page-content">
<div id="left-column"> <div id="left-column">
<div class="left-column-segment"> <div class="left-column-segment">
@ -52,14 +46,16 @@ import DefaultIcon from '~/components/DefaultIcon.vue';
import GuildDropdown from '~/components/Guild/GuildDropdown.vue'; import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
import Loading from '~/components/Popups/Loading.vue'; import Loading from '~/components/Popups/Loading.vue';
import Button from '~/components/UserInterface/Button.vue'; import Button from '~/components/UserInterface/Button.vue';
import Navbar from '~/components/UserInterface/Navbar.vue';
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue'; import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
import type { GuildResponse } from '~/types/interfaces'; import type { GuildResponse, NavbarInterface, NavbarItem } from '~/types/interfaces';
definePageMeta({ definePageMeta({
keepalive: true keepalive: true
}); });
const loading = useState("loading", () => false); const loading = useState("loading", () => false);
const navbar = useState<NavbarInterface>("navbar")
const createButtonContainer = ref<HTMLButtonElement>(); const createButtonContainer = ref<HTMLButtonElement>();
@ -68,60 +64,60 @@ const api = useApi();
const options = [ const options = [
{ name: "Join", value: "join", callback: async () => { { name: "Join", value: "join", callback: async () => {
console.log("join guild!"); console.log("join guild!");
const div = document.createElement("div"); const div = document.createElement("div");
const guildJoinModal = h(ModalBase, { const guildJoinModal = h(ModalBase, {
title: "Join Guild", title: "Join Guild",
id: "guild-join-modal", id: "guild-join-modal",
onClose: () => { onClose: () => {
unrender(div); unrender(div);
},
onCancel: () => {
unrender(div);
},
style: "height: 20dvh; width: 15dvw"
}, },
[ onCancel: () => {
h("input", { unrender(div);
id: "guild-invite-input", },
type: "text", style: "height: 20dvh; width: 15dvw"
placeholder: "oyqICZ", },
}), [
h(Button, { h("input", {
text: "Join", id: "guild-invite-input",
variant: "normal", type: "text",
callback: async () => { placeholder: "oyqICZ",
const input = document.getElementById("guild-invite-input") as HTMLInputElement; }),
const invite = input.value; h(Button, {
if (invite.length == 6) { text: "Join",
try { variant: "normal",
const joinedGuild = await api.joinGuild(invite); callback: async () => {
guilds.push(joinedGuild); const input = document.getElementById("guild-invite-input") as HTMLInputElement;
return await navigateTo(`/servers/${joinedGuild.uuid}`); const invite = input.value;
} catch (error) { if (invite.length == 6) {
alert(`Couldn't use invite: ${error}`); try {
} const joinedGuild = await api.joinGuild(invite);
guilds.push(joinedGuild);
return await navigateTo(`/servers/${joinedGuild.uuid}`);
} catch (error) {
alert(`Couldn't use invite: ${error}`);
} }
} }
}) }
]); })
document.body.appendChild(div); ]);
render(guildJoinModal, div); document.body.appendChild(div);
} render(guildJoinModal, div);
}, }
{ name: "Create", value: "create", callback: async () => { },
console.log("create guild"); { name: "Create", value: "create", callback: async () => {
const user = await useAuth().getUser(); console.log("create guild");
const div = document.createElement("div"); const user = await useAuth().getUser();
const guildCreateModal = h(ModalBase, { const div = document.createElement("div");
title: "Create a Guild", const guildCreateModal = h(ModalBase, {
id: "guild-join-modal", title: "Create a Guild",
onClose: () => { id: "guild-join-modal",
unrender(div); onClose: () => {
}, unrender(div);
onCancel: () => { },
unrender(div); onCancel: () => {
}, unrender(div);
},
style: "height: 20dvh; width: 15dvw;" style: "height: 20dvh; width: 15dvw;"
}, },
[ [
@ -154,6 +150,23 @@ const options = [
const guilds = await api.fetchMyGuilds(); const guilds = await api.fetchMyGuilds();
onMounted(() => {
if (!navbar.value) {
const helpItem = {
title: "Source",
icon: "lucide:code-xml",
callback: () => { open("https://git.gorb.app/gorb/frontend") }
} as NavbarItem
navbar.value = {
clientItems: [
helpItem
],
channelItems: [] // set by the channel
} as NavbarInterface
}
})
function createDropdown() { function createDropdown() {
const dropdown = h(GuildDropdown, { options }); const dropdown = h(GuildDropdown, { options });
const div = document.createElement("div"); const div = document.createElement("div");
@ -192,22 +205,6 @@ function createDropdown() {
transition: opacity 500ms; transition: opacity 500ms;
} }
#homebar {
min-height: 4dvh;
display: flex;
justify-content: space-evenly;
align-items: center;
background: var(--optional-topbar-background);
background-color: var(--topbar-background-color);
border-bottom: 1px solid var(--padding-color);
padding-left: 5dvw;
padding-right: 5dvw;
}
.homebar-item {
width: 100dvw;
}
#page-content { #page-content {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -36,11 +36,12 @@ import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue"; import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue";
import MemberEntry from "~/components/Guild/MemberEntry.vue"; import MemberEntry from "~/components/Guild/MemberEntry.vue";
import ResizableSidebar from "~/components/UserInterface/ResizableSidebar.vue"; import ResizableSidebar from "~/components/UserInterface/ResizableSidebar.vue";
import type { ChannelResponse, GuildMemberResponse, GuildResponse } from "~/types/interfaces"; import type { ChannelResponse, GuildMemberResponse, GuildResponse, NavbarInterface } from "~/types/interfaces";
const route = useRoute(); const route = useRoute();
const loading = useState("loading"); const loading = useState("loading");
const navbar = useState<NavbarInterface>("navbar");
const channelUrlPath = `channels/${route.params.channelId}`; const channelUrlPath = `channels/${route.params.channelId}`;
@ -65,13 +66,18 @@ onMounted(async () => {
console.log("fetched guild"); console.log("fetched guild");
await setArrayVariables(); await setArrayVariables();
console.log("set array variables"); console.log("set array variables");
updateNavbar()
}); });
onActivated(async () => { onActivated(async () => {
console.log("activating"); console.log("activating");
updateNavbar()
const guildUrl = `guilds/${route.params.serverId}`; const guildUrl = `guilds/${route.params.serverId}`;
server.value = await fetchWithApi(guildUrl); server.value = await fetchWithApi(guildUrl);
console.log("fetched guild"); console.log("fetched guild");
await setArrayVariables(); await setArrayVariables();
console.log("set array variables"); console.log("set array variables");
}); });
@ -98,6 +104,14 @@ function toggleInvitePopup(e: Event) {
function handleMemberClick(member: GuildMemberResponse) { function handleMemberClick(member: GuildMemberResponse) {
} }
function updateNavbar() {
if (server.value) {
navbar.value.contextName = server.value.name
navbar.value.contextIcon = server.value.icon ?? undefined
navbar.value.guildUuid = server.value.uuid
}
}
</script> </script>
<style> <style>

View file

@ -121,3 +121,19 @@ export interface ContextMenuInterface {
pointerY: number, pointerY: number,
items: ContextMenuItem[] items: ContextMenuItem[]
} }
export interface NavbarItem {
title: string,
icon: string,
hasPing?: boolean, // whether to draw a "ping" icon or not
callback: (...args: any[]) => any;
}
export interface NavbarInterface {
clientItems: NavbarItem[]
channelItems: NavbarItem[] // search bar will require some changes
contextName?: string
contextIcon?: string
guildUuid?: string
}