Refactor the client to implement a channel navbar #76
4 changed files with 227 additions and 76 deletions
124
components/UserInterface/Navbar.vue
Normal file
124
components/UserInterface/Navbar.vue
Normal 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>
|
|
@ -1,13 +1,7 @@
|
|||
<template>
|
||||
<Loading v-show="loading" />
|
||||
<div :class="{ hidden: loading, visible: !loading }" id="client-root">
|
||||
<div id="homebar">
|
||||
<div class="homebar-item">
|
||||
<marquee>
|
||||
gorb!!!!!
|
||||
</marquee>
|
||||
</div>
|
||||
</div>
|
||||
<Navbar id="navbar" v-bind="navbar" />
|
||||
<div id="page-content">
|
||||
<div id="left-column">
|
||||
<div class="left-column-segment">
|
||||
|
@ -52,14 +46,16 @@ import DefaultIcon from '~/components/DefaultIcon.vue';
|
|||
import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
|
||||
import Loading from '~/components/Popups/Loading.vue';
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
import Navbar from '~/components/UserInterface/Navbar.vue';
|
||||
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
||||
import type { GuildResponse } from '~/types/interfaces';
|
||||
import type { GuildResponse, NavbarInterface, NavbarItem } from '~/types/interfaces';
|
||||
|
||||
definePageMeta({
|
||||
keepalive: true
|
||||
});
|
||||
|
||||
const loading = useState("loading", () => false);
|
||||
const navbar = useState<NavbarInterface>("navbar")
|
||||
|
||||
const createButtonContainer = ref<HTMLButtonElement>();
|
||||
|
||||
|
@ -68,60 +64,60 @@ const api = useApi();
|
|||
|
||||
const options = [
|
||||
{ name: "Join", value: "join", callback: async () => {
|
||||
console.log("join guild!");
|
||||
const div = document.createElement("div");
|
||||
const guildJoinModal = h(ModalBase, {
|
||||
title: "Join Guild",
|
||||
id: "guild-join-modal",
|
||||
onClose: () => {
|
||||
unrender(div);
|
||||
},
|
||||
onCancel: () => {
|
||||
unrender(div);
|
||||
},
|
||||
style: "height: 20dvh; width: 15dvw"
|
||||
console.log("join guild!");
|
||||
const div = document.createElement("div");
|
||||
const guildJoinModal = h(ModalBase, {
|
||||
title: "Join Guild",
|
||||
id: "guild-join-modal",
|
||||
onClose: () => {
|
||||
unrender(div);
|
||||
},
|
||||
[
|
||||
h("input", {
|
||||
id: "guild-invite-input",
|
||||
type: "text",
|
||||
placeholder: "oyqICZ",
|
||||
}),
|
||||
h(Button, {
|
||||
text: "Join",
|
||||
variant: "normal",
|
||||
callback: async () => {
|
||||
const input = document.getElementById("guild-invite-input") as HTMLInputElement;
|
||||
const invite = input.value;
|
||||
if (invite.length == 6) {
|
||||
try {
|
||||
const joinedGuild = await api.joinGuild(invite);
|
||||
guilds.push(joinedGuild);
|
||||
return await navigateTo(`/servers/${joinedGuild.uuid}`);
|
||||
} catch (error) {
|
||||
alert(`Couldn't use invite: ${error}`);
|
||||
}
|
||||
onCancel: () => {
|
||||
unrender(div);
|
||||
},
|
||||
style: "height: 20dvh; width: 15dvw"
|
||||
},
|
||||
[
|
||||
h("input", {
|
||||
id: "guild-invite-input",
|
||||
type: "text",
|
||||
placeholder: "oyqICZ",
|
||||
}),
|
||||
h(Button, {
|
||||
text: "Join",
|
||||
variant: "normal",
|
||||
callback: async () => {
|
||||
const input = document.getElementById("guild-invite-input") as HTMLInputElement;
|
||||
const invite = input.value;
|
||||
if (invite.length == 6) {
|
||||
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);
|
||||
}
|
||||
},
|
||||
{ name: "Create", value: "create", callback: async () => {
|
||||
console.log("create guild");
|
||||
const user = await useAuth().getUser();
|
||||
const div = document.createElement("div");
|
||||
const guildCreateModal = h(ModalBase, {
|
||||
title: "Create a Guild",
|
||||
id: "guild-join-modal",
|
||||
onClose: () => {
|
||||
unrender(div);
|
||||
},
|
||||
onCancel: () => {
|
||||
unrender(div);
|
||||
},
|
||||
}
|
||||
})
|
||||
]);
|
||||
document.body.appendChild(div);
|
||||
render(guildJoinModal, div);
|
||||
}
|
||||
},
|
||||
{ name: "Create", value: "create", callback: async () => {
|
||||
console.log("create guild");
|
||||
const user = await useAuth().getUser();
|
||||
const div = document.createElement("div");
|
||||
const guildCreateModal = h(ModalBase, {
|
||||
title: "Create a Guild",
|
||||
id: "guild-join-modal",
|
||||
onClose: () => {
|
||||
unrender(div);
|
||||
},
|
||||
onCancel: () => {
|
||||
unrender(div);
|
||||
},
|
||||
style: "height: 20dvh; width: 15dvw;"
|
||||
},
|
||||
[
|
||||
|
@ -154,6 +150,23 @@ const options = [
|
|||
|
||||
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() {
|
||||
const dropdown = h(GuildDropdown, { options });
|
||||
const div = document.createElement("div");
|
||||
|
@ -192,22 +205,6 @@ function createDropdown() {
|
|||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -36,11 +36,12 @@ import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
|
|||
import GuildOptionsMenu from "~/components/Guild/GuildOptionsMenu.vue";
|
||||
import MemberEntry from "~/components/Guild/MemberEntry.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 loading = useState("loading");
|
||||
const navbar = useState<NavbarInterface>("navbar");
|
||||
|
||||
const channelUrlPath = `channels/${route.params.channelId}`;
|
||||
|
||||
|
@ -65,13 +66,18 @@ onMounted(async () => {
|
|||
console.log("fetched guild");
|
||||
await setArrayVariables();
|
||||
console.log("set array variables");
|
||||
|
||||
updateNavbar()
|
||||
});
|
||||
|
||||
onActivated(async () => {
|
||||
console.log("activating");
|
||||
updateNavbar()
|
||||
|
||||
const guildUrl = `guilds/${route.params.serverId}`;
|
||||
server.value = await fetchWithApi(guildUrl);
|
||||
console.log("fetched guild");
|
||||
|
||||
await setArrayVariables();
|
||||
console.log("set array variables");
|
||||
});
|
||||
|
@ -98,6 +104,14 @@ function toggleInvitePopup(e: Event) {
|
|||
|
||||
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>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -121,3 +121,19 @@ export interface ContextMenuInterface {
|
|||
pointerY: number,
|
||||
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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue