Refactor the client to implement a channel navbar #76

Open
twig wants to merge 23 commits from navbar into main
4 changed files with 227 additions and 76 deletions
Showing only changes of commit 28f5e8dc27 - Show all commits

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 */
twig marked this conversation as resolved Outdated

Shouldn't these variable definitions be defined in theme files?

Shouldn't these variable definitions be defined in theme files?

i suppose? they're really just there to make development easier, but they could be included in the layout theme files, if so, do we include all 4?

i suppose? they're really just there to make development easier, but they could be included in the layout theme files, if so, do we include all 4?

Yeah, don't see a reason not to

Yeah, don't see a reason not to
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>();
twig marked this conversation as resolved Outdated

This should be an anchor element, not a button

This should be an anchor element, not a button

disagree, i'd like to put an inbox here, that would be a popup, not a link

disagree, i'd like to put an inbox here, that would be a popup, not a link

Right but I think all clickable elements that lead you to an external site should be anchor elements. Here I'm specifically talking about the source code element.

Right but I think all clickable elements that lead you to an external site should be anchor elements. Here I'm specifically talking about the source code element.

sure, i'm reworking it anyways

sure, i'm reworking it anyways
@ -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!");
twig marked this conversation as resolved Outdated

what the hell did codium decide to do here? i proomise i didn't touch it

what the hell did codium decide to do here? i proomise i didn't touch it
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
}