Refactor the client to implement a channel navbar #76
13 changed files with 522 additions and 396 deletions
85
components/Guild/ChannelNavbar.vue
Normal file
85
components/Guild/ChannelNavbar.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div id="navbar" class="navbar-row">
|
||||||
|
<div id="channel-info" class="navbar-row">
|
||||||
|
<!-- h1 to help screen readers -->
|
||||||
|
<h1 id="channel-name">
|
||||||
|
# {{ channel.name }}
|
||||||
|
</h1>
|
||||||
|
<span id="channel-description">
|
||||||
|
{{ channel.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="buttons" class="navbar-row">
|
||||||
|
<a class="button" href="https://git.gorb.app/gorb/frontend">
|
||||||
|
<Icon name="lucide:code-xml" title="Source"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { INavbar } from '~/types/interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<INavbar>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navbar-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar {
|
||||||
|
--font-size: calc(var(--navbar-height) * 0.45);
|
||||||
|
--side-margins: calc(var(--font-size) * 0.5);
|
||||||
|
|
||||||
|
min-height: var(--navbar-height);
|
||||||
|
max-height: var(--navbar-height);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background: var(--optional-topbar-background);
|
||||||
|
background-color: var(--topbar-background-color);
|
||||||
|
border-bottom: 1px solid var(--padding-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#channel-info {
|
||||||
|
margin-left: var(--side-margins);
|
||||||
|
gap: calc(var(--font-size) * 0.75);
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#channel-name {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#channel-description {
|
||||||
|
font-size: calc(var(--font-size) * 0.8);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
/* TODO make new theme colour? unsure of it's name, this is good enough for now */
|
||||||
|
color: var(--reply-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
margin-right: var(--side-margins);
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
transition: color 300ms;
|
||||||
|
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
color: var(--primary-highlighted-color);
|
||||||
|
}
|
||||||
|
</style>
|
91
components/Guild/GuildSidebar.vue
Normal file
91
components/Guild/GuildSidebar.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<ResizableSidebar
|
||||||
|
width="14rem" min-width="8rem" max-width="30rem"
|
||||||
|
border-sides="right" local-storage-name="middleLeftColumn">
|
||||||
|
|
||||||
|
<div id="guild-sidebar">
|
||||||
|
<div id="guild-top-container">
|
||||||
|
<span id="guild-name" :title="props.guild?.name">{{ props.guild?.name }}</span>
|
||||||
|
<button id="guild-settings-button" @click="toggleGuildSettings">
|
||||||
|
<Icon name="lucide:chevron-down" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<GuildOptionsMenu v-if="showGuildSettings" />
|
||||||
|
</div>
|
||||||
|
<div id="channels-list">
|
||||||
|
<ChannelEntry v-for="channel of channels"
|
||||||
|
:name="channel.name"
|
||||||
|
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
|
||||||
|
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizableSidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
|
||||||
|
import ResizableSidebar from "../UserInterface/ResizableSidebar.vue";
|
||||||
|
import type { ChannelResponse, GuildResponse, INavbar } from "~/types/interfaces";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
guild: GuildResponse
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { fetchChannels } = useApi();
|
||||||
|
|
||||||
|
const showGuildSettings = ref(false);
|
||||||
|
|
||||||
|
const channels: ChannelResponse[] = await fetchChannels(props.guild.uuid);
|
||||||
|
|
||||||
|
function toggleGuildSettings(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
showGuildSettings.value = !showGuildSettings.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#guild-top-container {
|
||||||
|
min-height: var(--navbar-height);
|
||||||
|
max-height: var(--navbar-height);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--padding-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#guild-name {
|
||||||
|
font-size: 1.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guild-settings-button {
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 1em;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#channels-list {
|
||||||
|
background: var(--optional-channel-list-background);
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
padding-top: .5em;
|
||||||
|
padding-bottom: .5em;
|
||||||
|
max-height: calc(100% - 1em); /* 100% - top and bottom padding */
|
||||||
|
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -32,7 +32,13 @@ function hideModalPopup() {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.member-item {
|
.member-item {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
margin-top: .5em;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
gap: .5em;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-avatar {
|
.member-avatar {
|
||||||
|
@ -41,4 +47,9 @@ function hideModalPopup() {
|
||||||
min-width: 2.3em;
|
min-width: 2.3em;
|
||||||
max-width: 2.3em;
|
max-width: 2.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-display-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
47
components/Guild/MemberList.vue
Normal file
47
components/Guild/MemberList.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<ResizableSidebar
|
||||||
|
width="14rem" min-width="5.5rem" max-width="30rem"
|
||||||
|
border-sides="left" local-storage-name="membersListWidth">
|
||||||
|
<div id="members-container">
|
||||||
|
<div id="members-list">
|
||||||
|
<MemberEntry v-for="member of members.objects" :member="member" tabindex="0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizableSidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ResizableSidebar from "../UserInterface/ResizableSidebar.vue";
|
||||||
|
import MemberEntry from "./MemberEntry.vue";
|
||||||
|
import type { GuildResponse } from "~/types/interfaces";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
guild: GuildResponse
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { fetchMembers } = useApi();
|
||||||
|
|
||||||
|
// TODO implement paging
|
||||||
|
const members = await fetchMembers(props.guild.uuid)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#members-container {
|
||||||
|
background: var(--optional-member-list-background);
|
||||||
|
|
||||||
|
padding-top: .5em;
|
||||||
|
padding-bottom: .5em;
|
||||||
|
max-height: calc(100% - 1em); /* 100% - top and bottom padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
#members-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -45,6 +45,8 @@ import { generateIrcColor } from '#imports';
|
||||||
const { getDisplayName } = useProfile()
|
const { getDisplayName } = useProfile()
|
||||||
const { fetchMe } = useApi()
|
const { fetchMe } = useApi()
|
||||||
|
|
||||||
|
// TODO this file is a mess, and we need to stop using fetchWithApi
|
||||||
|
|
||||||
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
||||||
|
|
||||||
const me = await fetchMe() as UserResponse;
|
const me = await fetchMe() as UserResponse;
|
||||||
|
|
|
@ -135,8 +135,7 @@ function loadStoredWidth() {
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: .25em;
|
height: 100%;
|
||||||
padding-right: .25em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-content > :first-child {
|
.sidebar-content > :first-child {
|
||||||
|
|
218
components/UserInterface/SidebarColumn.vue
Normal file
218
components/UserInterface/SidebarColumn.vue
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
<template>
|
||||||
|
<div id="sidebar-column">
|
||||||
|
<div class="side-column-segment">
|
||||||
|
<NuxtLink id="home-button" href="/me">
|
||||||
|
<img class="sidebar-icon" src="/public/icon.svg"/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VerticalSpacer />
|
||||||
|
<div class="sidebar-column-segment" id="guild-column">
|
||||||
|
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`" id="guild-icon-container">
|
||||||
|
<NuxtImg v-if="guild.icon"
|
||||||
|
class="sidebar-icon guild-icon"
|
||||||
|
:alt="guild.name"
|
||||||
|
:src="guild.icon" />
|
||||||
|
<DefaultIcon v-else
|
||||||
|
class="sidebar-icon guild-icon"
|
||||||
|
:alt="guild.name"
|
||||||
|
:name="guild.name" :seed="guild.uuid"/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<VerticalSpacer />
|
||||||
|
|
||||||
|
<div class="sidebar-column-segment">
|
||||||
|
<div ref="createButtonContainer">
|
||||||
|
<button id="create-button" class="sidebar-bottom-buttons" @click.prevent="createDropdown">
|
||||||
|
<Icon id="create-icon" name="lucide:square-plus" alt="Create or join guild"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<NuxtLink id="settings-menu" class="sidebar-bottom-buttons" href="/settings">
|
||||||
|
<Icon name="lucide:settings" alt="Settings menu" />
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ModalBase } from '#components';
|
||||||
|
import { render } from 'vue';
|
||||||
|
import GuildDropdown from '~/components/Guild/GuildDropdown.vue';
|
||||||
|
import DefaultIcon from '~/components/DefaultIcon.vue';
|
||||||
|
import Button from '~/components/UserInterface/Button.vue';
|
||||||
|
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
||||||
|
import type { GuildResponse } from '~/types/interfaces';
|
||||||
|
|
||||||
|
const { getDisplayName } = useProfile()
|
||||||
|
const { fetchMyGuilds, joinGuild, createGuild, createChannel } = useApi();
|
||||||
|
|
||||||
|
const createButtonContainer = ref<HTMLButtonElement>();
|
||||||
|
|
||||||
|
const guilds = await fetchMyGuilds();
|
||||||
|
|
||||||
|
// TODO we need to turn this into an actual modal
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
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 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);
|
||||||
|
},
|
||||||
|
style: "height: 20dvh; width: 15dvw;"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("input", {
|
||||||
|
id: "guild-name-input",
|
||||||
|
type: "text",
|
||||||
|
placeholder: `${getDisplayName(user!)}'s Awesome Bouncy Castle'`,
|
||||||
|
style: "width: 100%"
|
||||||
|
}),
|
||||||
|
h(Button, {
|
||||||
|
text: "Create!",
|
||||||
|
variant: "normal",
|
||||||
|
callback: async () => {
|
||||||
|
const input = document.getElementById("guild-name-input") as HTMLInputElement;
|
||||||
|
const name = input.value;
|
||||||
|
try {
|
||||||
|
const guild = (await createGuild(name)) as GuildResponse;
|
||||||
|
await createChannel(guild.uuid, "general");
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Couldn't create guild: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
document.body.appendChild(div);
|
||||||
|
render(guildCreateModal, div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function createDropdown() {
|
||||||
|
const dropdown = h(GuildDropdown, { options });
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.classList.add("dropdown", "destroy-on-click");
|
||||||
|
if (createButtonContainer.value) {
|
||||||
|
createButtonContainer.value.appendChild(div);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
render(dropdown, div);
|
||||||
|
div.addEventListener("keyup", (e) => {
|
||||||
|
if (e.key == "Escape") {
|
||||||
|
unrender(div);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
div.focus();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#sidebar-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding-left: var(--sidebar-margin);
|
||||||
|
padding-right: var(--sidebar-margin);
|
||||||
|
padding-top: .5em;
|
||||||
|
|
||||||
|
background: var(--optional-sidebar-background);
|
||||||
|
background-color: var(--sidebar-background-color);
|
||||||
|
|
||||||
|
border-right: 1px solid var(--padding-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guild-column {
|
||||||
|
overflow-y: scroll;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: var(--sidebar-icon-gap);
|
||||||
|
}
|
||||||
|
#guild-icon-container {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-column-segment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.guild-column-segment::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#home-button {
|
||||||
|
height: var(--sidebar-icon-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-icon {
|
||||||
|
border-radius: var(--guild-icon-radius);
|
||||||
|
}
|
||||||
|
.sidebar-icon {
|
||||||
|
width: var(--sidebar-icon-width);
|
||||||
|
height: var(--sidebar-icon-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-bottom-buttons {
|
||||||
|
color: var(--primary-color);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.sidebar-bottom-buttons:hover {
|
||||||
|
color: var(--primary-highlighted-color);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,59 +1,17 @@
|
||||||
<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">
|
<div class="flex-container-row">
|
||||||
<div class="homebar-item">
|
<SidebarColumn />
|
||||||
<marquee>
|
|
||||||
gorb!!!!!
|
|
||||||
</marquee>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="page-content">
|
|
||||||
<div id="left-column">
|
|
||||||
<div class="left-column-segment">
|
|
||||||
<NuxtLink id="home-button" href="/me">
|
|
||||||
<img class="sidebar-icon" src="/public/icon.svg"/>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<VerticalSpacer />
|
|
||||||
<div class="left-column-segment" id="left-column-middle">
|
|
||||||
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`" id="guild-icon-container">
|
|
||||||
<NuxtImg v-if="guild.icon"
|
|
||||||
class="sidebar-icon guild-icon"
|
|
||||||
:alt="guild.name"
|
|
||||||
:src="guild.icon" />
|
|
||||||
<DefaultIcon v-else
|
|
||||||
class="sidebar-icon guild-icon"
|
|
||||||
:alt="guild.name"
|
|
||||||
:name="guild.name" :seed="guild.uuid"/>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<VerticalSpacer />
|
|
||||||
<div class="left-column-segment">
|
|
||||||
<div ref="createButtonContainer">
|
|
||||||
<button id="create-button" class="sidebar-bottom-buttons" @click.prevent="createDropdown">
|
|
||||||
<Icon id="create-icon" name="lucide:square-plus" alt="Create or join guild"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<NuxtLink id="settings-menu" class="sidebar-bottom-buttons" href="/settings">
|
|
||||||
<Icon name="lucide:settings" alt="Settings menu" />
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ModalBase } from '#components';
|
|
||||||
import { render } from 'vue';
|
|
||||||
import DefaultIcon from '~/components/DefaultIcon.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 SidebarColumn from '~/components/UserInterface/SidebarColumn.vue';
|
||||||
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
|
||||||
import type { GuildResponse } from '~/types/interfaces';
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
keepalive: true
|
keepalive: true
|
||||||
|
@ -61,117 +19,6 @@ definePageMeta({
|
||||||
|
|
||||||
const loading = useState("loading", () => false);
|
const loading = useState("loading", () => false);
|
||||||
|
|
||||||
const createButtonContainer = ref<HTMLButtonElement>();
|
|
||||||
|
|
||||||
const { getDisplayName } = useProfile()
|
|
||||||
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"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
style: "height: 20dvh; width: 15dvw;"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h("input", {
|
|
||||||
id: "guild-name-input",
|
|
||||||
type: "text",
|
|
||||||
placeholder: `${getDisplayName(user!)}'s Awesome Bouncy Castle'`,
|
|
||||||
style: "width: 100%"
|
|
||||||
}),
|
|
||||||
h(Button, {
|
|
||||||
text: "Create!",
|
|
||||||
variant: "normal",
|
|
||||||
callback: async () => {
|
|
||||||
const input = document.getElementById("guild-name-input") as HTMLInputElement;
|
|
||||||
const name = input.value;
|
|
||||||
try {
|
|
||||||
const guild = (await api.createGuild(name)) as GuildResponse;
|
|
||||||
await api.createChannel(guild.uuid, "general");
|
|
||||||
} catch (error) {
|
|
||||||
alert(`Couldn't create guild: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
document.body.appendChild(div);
|
|
||||||
render(guildCreateModal, div);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const guilds = await api.fetchMyGuilds();
|
|
||||||
|
|
||||||
function createDropdown() {
|
|
||||||
const dropdown = h(GuildDropdown, { options });
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.classList.add("dropdown", "destroy-on-click");
|
|
||||||
if (createButtonContainer.value) {
|
|
||||||
createButtonContainer.value.appendChild(div);
|
|
||||||
} else {
|
|
||||||
document.body.appendChild(div);
|
|
||||||
}
|
|
||||||
render(dropdown, div);
|
|
||||||
div.addEventListener("keyup", (e) => {
|
|
||||||
if (e.key == "Escape") {
|
|
||||||
unrender(div);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
div.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -179,8 +26,6 @@ function createDropdown() {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
width: 100dvw;
|
width: 100dvw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
|
@ -192,89 +37,20 @@ 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 {
|
.flex-container-row,
|
||||||
width: 100dvw;
|
.flex-container-column {
|
||||||
}
|
|
||||||
|
|
||||||
#page-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
|
||||||
|
|
||||||
#left-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
padding-left: var(--sidebar-margin);
|
|
||||||
padding-right: var(--sidebar-margin);
|
|
||||||
padding-top: .5em;
|
|
||||||
|
|
||||||
background: var(--optional-sidebar-background);
|
|
||||||
background-color: var(--sidebar-background-color);
|
|
||||||
|
|
||||||
border-right: 1px solid var(--padding-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-column-segment {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-column-segment::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#left-column-middle {
|
|
||||||
overflow-y: scroll;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
gap: var(--sidebar-icon-gap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-button {
|
.flex-container-row {
|
||||||
height: var(--sidebar-icon-width);
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#guild-icon-container {
|
.flex-container-column {
|
||||||
text-decoration: none;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.guild-icon {
|
|
||||||
border-radius: var(--guild-icon-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-icon {
|
|
||||||
width: var(--sidebar-icon-width);
|
|
||||||
height: var(--sidebar-icon-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-bottom-buttons {
|
|
||||||
color: var(--primary-color);
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 2.4rem;
|
|
||||||
padding: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-bottom-buttons:hover {
|
|
||||||
color: var(--primary-highlighted-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
<template>
|
|
||||||
<NuxtLayout>
|
|
||||||
|
|
||||||
</NuxtLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
await navigateTo("/me/", { replace: true })
|
await navigateTo("/me/", { replace: true })
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: "client"
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -8,6 +8,14 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
updateNavbar({isDirectMessages: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
updateNavbar({isDirectMessages: true})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,163 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="client">
|
<NuxtLayout name="client">
|
||||||
<ResizableSidebar
|
<GuildSidebar v-if="guild" :guild="guild" />
|
||||||
width="14rem" min-width="8rem" max-width="30rem"
|
<div class="flex-container-column">
|
||||||
border-sides="right" local-storage-name="middleLeftColumn">
|
<GuildChannelNavbar id="navbar"
|
||||||
<div id="middle-left-column" class="main-grid-row">
|
v-if="guild && channel"
|
||||||
<div id="server-name-container">
|
:guild="guild"
|
||||||
<span id="server-name" :title="server?.name">{{ server?.name }}</span>
|
:channel="channel"/>
|
||||||
<button id="server-settings-button" @click="toggleGuildSettings">
|
|
||||||
<Icon id="server-settings-icon" name="lucide:chevron-down" />
|
<div class="flex-container-row">
|
||||||
</button>
|
<MessageArea :channel-url="channelUrlPath" />
|
||||||
<GuildOptionsMenu v-if="showGuildSettings" />
|
<GuildMemberList v-if="guild" :guild="guild" />
|
||||||
</div>
|
|
||||||
<div id="channels-list">
|
|
||||||
<ChannelEntry v-for="channel of channels" :name="channel.name"
|
|
||||||
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
|
|
||||||
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizableSidebar>
|
</div>
|
||||||
<MessageArea :channel-url="channelUrlPath" />
|
|
||||||
<ResizableSidebar
|
|
||||||
width="14rem" min-width="5.5rem" max-width="30rem"
|
|
||||||
border-sides="left" local-storage-name="membersListWidth">
|
|
||||||
<div id="members-container">
|
|
||||||
<div id="members-list">
|
|
||||||
<MemberEntry v-for="member of members" :member="member" tabindex="0"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResizableSidebar>
|
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
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";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { fetchGuild, fetchChannel } = useApi()
|
||||||
|
|
||||||
const loading = useState("loading");
|
const channelId = route.params.channelId as string
|
||||||
|
const guildId = route.params.serverId as string
|
||||||
|
|
||||||
const channelUrlPath = `channels/${route.params.channelId}`;
|
const channelUrlPath = `channels/${channelId}`;
|
||||||
|
|
||||||
const server = ref<GuildResponse | undefined>();
|
const guild = await fetchGuild(guildId)
|
||||||
const channels = ref<ChannelResponse[] | undefined>();
|
const channel = await fetchChannel(channelId)
|
||||||
const channel = ref<ChannelResponse | undefined>();
|
|
||||||
|
|
||||||
const members = ref<GuildMemberResponse[]>();
|
// function toggleInvitePopup(e: Event) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// showInvitePopup.value = !showInvitePopup.value;
|
||||||
|
// }
|
||||||
|
|
||||||
const showInvitePopup = ref(false);
|
// function handleMemberClick(member: GuildMemberResponse) {
|
||||||
const showGuildSettings = ref(false);
|
// }
|
||||||
|
|
||||||
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
|
|
||||||
//console.log("channelid: servers:", servers);
|
|
||||||
|
|
||||||
const { fetchMembers } = useApi();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
console.log("mounting");
|
|
||||||
const guildUrl = `guilds/${route.params.serverId}`;
|
|
||||||
server.value = await fetchWithApi(guildUrl);
|
|
||||||
console.log("fetched guild");
|
|
||||||
await setArrayVariables();
|
|
||||||
console.log("set array variables");
|
|
||||||
});
|
|
||||||
|
|
||||||
onActivated(async () => {
|
|
||||||
console.log("activating");
|
|
||||||
const guildUrl = `guilds/${route.params.serverId}`;
|
|
||||||
server.value = await fetchWithApi(guildUrl);
|
|
||||||
console.log("fetched guild");
|
|
||||||
await setArrayVariables();
|
|
||||||
console.log("set array variables");
|
|
||||||
});
|
|
||||||
|
|
||||||
async function setArrayVariables() {
|
|
||||||
const membersRes = await fetchMembers(route.params.serverId as string);
|
|
||||||
members.value = membersRes.objects;
|
|
||||||
const guildUrl = `guilds/${route.params.serverId}`;
|
|
||||||
channels.value = await fetchWithApi(`${guildUrl}/channels`);
|
|
||||||
console.log("channels:", channels.value);
|
|
||||||
channel.value = await fetchWithApi(`/channels/${route.params.channelId}`);
|
|
||||||
console.log("channel:", channel.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleGuildSettings(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
showGuildSettings.value = !showGuildSettings.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleInvitePopup(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
showInvitePopup.value = !showInvitePopup.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMemberClick(member: GuildMemberResponse) {
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
#members-container {
|
|
||||||
background: var(--optional-member-list-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
#members-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
padding-left: 1.25em;
|
|
||||||
padding-right: 1.25em;
|
|
||||||
padding-top: 0.75em;
|
|
||||||
padding-bottom: 0.75em;
|
|
||||||
max-height: calc(100% - 0.75em * 2); /* 100% - top and bottom */
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-item {
|
|
||||||
display: flex;
|
|
||||||
margin-top: .5em;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
gap: .5em;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#channels-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .5em;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-display-name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
#server-name-container {
|
|
||||||
padding-top: 3dvh;
|
|
||||||
padding-bottom: 3dvh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#server-name {
|
|
||||||
font-size: 1.5em;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
#server-settings-button {
|
|
||||||
background-color: transparent;
|
|
||||||
font-size: 1em;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -9,6 +9,10 @@ complementaryColor = white
|
||||||
--sidebar-icon-gap: .25em;
|
--sidebar-icon-gap: .25em;
|
||||||
--sidebar-margin: .5em;
|
--sidebar-margin: .5em;
|
||||||
|
|
||||||
|
--navbar-height: 5dvh;
|
||||||
|
--navbar-icon-size: 3dvh;
|
||||||
|
--navbar-gap: calc(3dvh * .2);
|
||||||
|
|
||||||
--minor-radius: .35em;
|
--minor-radius: .35em;
|
||||||
--standard-radius: .5em;
|
--standard-radius: .5em;
|
||||||
--button-radius: .6em;
|
--button-radius: .6em;
|
||||||
|
|
|
@ -121,3 +121,22 @@ 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 INavbar {
|
||||||
|
guild: GuildResponse
|
||||||
|
channel: ChannelResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarOptions {
|
||||||
|
guild?: GuildResponse
|
||||||
|
channel?: ChannelResponse
|
||||||
|
isDirectMessages?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue