Compare commits

...

7 commits

Author SHA1 Message Date
0562127e4a
feat: finish entire friends menu
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-11 01:45:24 +02:00
34976b4f50
feat: finish DM sidebar 2025-07-10 23:12:44 +02:00
c9bea94ef8
fix: add missing imports 2025-07-10 23:03:46 +02:00
a3feb07e73
refactor: rename Channel to ChannelEntry 2025-07-10 23:00:54 +02:00
7b62d352f8
fix: user verticalSpacer over existing spans 2025-07-10 22:52:16 +02:00
8e69dc805e
chore: remove unused popups 2025-07-10 22:51:56 +02:00
59000709fe
feat: update api 2025-07-10 22:51:14 +02:00
17 changed files with 317 additions and 82 deletions

View file

@ -1,38 +0,0 @@
<template>
<div id="friend-sidebar">
<div id="server-title">
<h3>Direct Messages</h3>
</div>
<verticalSpacer />
<div id="direct-message-list">
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
:href="`/me/${user.uuid}`"/>
<!-- <Channel 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>
</template>
<script lang="ts" setup>
const { fetchFriends } = useApi();
const friends = await fetchFriends()
console.log(friends)
definePageMeta({
layout: "client"
});
</script>
<style>
#friend-sidebar {
padding-left: .5em;
padding-right: .5em;
border-right: 1px solid var(--padding-color);
background: var(--optional-channel-list-background);
background-color: var(--sidebar-background-color);
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<div style="text-align: left;">
<h3>Add a Friend</h3>
Enter a friend's Gorb ID to send them a friend request.
</div>
<div id="add-friend-search-bar">
<input id="add-friend-search-input" ref="inputField"
placeholder="blahaj.enjoyer?" maxlength="32" @keypress.enter="sendRequest"/> <!-- REMEMBER TO CHANGE THIS WHEN WE ADD FEDERATION-->
<Button id="friend-request-button" :callback="sendRequest" text="Send Friend Request"></Button>
</div>
</template>
<script lang="ts" setup>
import Button from '../UserInterface/Button.vue';
const inputField = ref<HTMLInputElement>();
const { addFriend } = useApi();
async function sendRequest() {
if (inputField.value) {
try {
await addFriend(inputField.value.value)
alert("Friend request sent!")
} catch {
alert("Request failed :(")
}
}
}
</script>
<style>
#add-friend-search-bar {
display: flex;
text-align: left;
margin-top: .8em;
padding: .3em .3em;
border-radius: 1em;
border: 1px solid var(--accent-color);
}
#add-friend-search-input {
border: none;
box-sizing: border-box;
margin: 0 .2em;
flex-grow: 1;
color: inherit;
background-color: unset;
font-weight: medium;
letter-spacing: .04em;
}
#add-friend-search-input:empty:before {
content: attr(placeholder);
color: gray;
}
</style>

View file

@ -0,0 +1,49 @@
<template>
<div id="middle-left-column">
<div id="friend-sidebar">
<div id="server-title">
<h3>Direct Messages</h3>
</div>
<VerticalSpacer />
<NuxtLink class="user-item" :href="`/me/friends`" tabindex="0">
<Icon class="user-avatar" name="lucide:user" />
<span class="user-display-name">Friends</span>
</NuxtLink>
<VerticalSpacer />
<div id="direct-message-list">
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
:href="`/me/${user.uuid}`"/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
const { fetchFriends } = useApi();
const friends = await fetchFriends()
console.log(friends)
definePageMeta({
layout: "client"
});
</script>
<style>
#middle-left-column {
background: var(--optional-channel-list-background);
background-color: var(--sidebar-background-color);
}
#friend-sidebar {
padding-left: .5em;
padding-right: .5em;
}
</style>

View file

@ -0,0 +1,61 @@
<template>
<input id="search-friend-bar" placeholder="search"/>
<!-- we aren't checking for the #all hash, since this is the default and fallback one -->
<p v-if="props.variant === '#online'" style="text-align: left;">Online {{ "N/A" }}</p>
<p v-else-if="props.variant === '#pending'" style="text-align: left;">Friend Requests {{ "N/A" }}</p>
<p v-else style="text-align: left;">Friends {{ friends?.length || "N/A" }}</p>
<div id="friends-list">
<div v-if="props.variant === '#online'">
Not Implemented
</div>
<div v-else-if="props.variant === '#pending'">
Not Implemented
</div>
<div v-else>
<UserEntry v-for="user of friends" :user="user" :name="user.display_name || user.username"
:href="`/me/${user.uuid}`"/>
</div>
</div>
</template>
<script lang="ts" setup>
const { fetchFriends } = useApi();
const friends = await fetchFriends()
const props = defineProps<{
variant: string
}>();
console.log("props", props.variant)
</script>
<style>
#search-friend-bar {
text-align: left;
margin-top: .8em;
padding: .3em .5em;
width: 100%;
border-radius: 1em;
border: 1px solid var(--accent-color);
box-sizing: border-box;
color: inherit;
background-color: unset;
font-weight: medium;
letter-spacing: .04em;
}
#search-friend-bar:empty:before {
content: attr(placeholder);
color: gray;
}
</style>

View file

@ -10,7 +10,6 @@
<script lang="ts" setup>
import { ref } from 'vue';
import type { GuildMemberResponse } from '~/types/interfaces';
import UserPopup from './UserPopup.vue';
const props = defineProps<{
member: GuildMemberResponse

View file

@ -17,7 +17,7 @@
</template>
<script lang="ts" setup>
import Button from '~/components/Button.vue';
import Button from '~/components/UserInterface/Button.vue';
import type { UserResponse } from '~/types/interfaces';
const { fetchUser } = useAuth();

View file

@ -5,7 +5,6 @@
</template>
<script lang="ts" setup>
import Button from '~/components/Button.vue';
</script>
<style scoped>

View file

@ -33,6 +33,9 @@
</template>
<script lang="ts" setup>
import UserPopup from '~/components/User/UserPopup.vue';
import Button from '~/components/UserInterface/Button.vue';
import type { UserResponse } from '~/types/interfaces';
let newPfpFile: File;

View file

@ -24,7 +24,12 @@ const props = defineProps<{
margin-bottom: .5em;
gap: .5em;
cursor: pointer;
text-decoration: none;
color: inherit;
}
.user-item:hover {
background-color: #00000020
}
.user-avatar {

View file

@ -2,15 +2,6 @@
<span class="spacer"></span>
</template>
<script lang="ts" setup>
import type { UserResponse } from '~/types/interfaces';
const props = defineProps<{
user: UserResponse,
}>();
</script>
<style scoped>
.spacer {
height: 0.2dvh;

View file

@ -1,4 +1,4 @@
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse } from "~/types/interfaces";
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse, UserResponse } from "~/types/interfaces";
export const useApi = () => {
async function fetchGuilds(): Promise<GuildResponse[] | undefined> {
@ -24,14 +24,26 @@ export const useApi = () => {
async function fetchMember(guildId: string, memberId: string): Promise<GuildMemberResponse | undefined> {
return await fetchWithApi(`/guilds/${guildId}/members/${memberId}`);
}
async function fetchUsers() {
return await fetchWithApi(`/users`);
}
async function fetchUser(userId: string) {
return await fetchWithApi(`/users/${userId}`);
}
async function fetchFriends(): Promise<UserResponse[] | undefined> {
return await fetchWithApi('/me/friends')
}
async function addFriend(username: string): Promise<void> {
await fetchWithApi('/me/friends', { method: "POST", body: { username } });
}
async function removeFriend(userId: string): Promise<void> {
await fetchWithApi(`/me/friends/${userId}`, { method: "DELETE" });
}
async function fetchMessages(channelId: string, options?: { amount?: number, offset?: number }): Promise<MessageResponse[] | undefined> {
return await fetchWithApi(`/channels/${channelId}/messages`, { query: { amount: options?.amount ?? 100, offset: options?.offset ?? 0 } });
@ -59,6 +71,9 @@ export const useApi = () => {
fetchMember,
fetchUsers,
fetchUser,
fetchFriends,
addFriend,
removeFriend,
fetchMessages,
fetchMessage,
fetchInstanceStats,

20
pages/me/[userId].vue Normal file
View file

@ -0,0 +1,20 @@
<template>
<NuxtLayout name="client">
<DirectMessagesSidebar />
<MessageArea channel-url="channels/01970e8c-a09c-76a0-9c98-80a43364bea7"/>
</NuxtLayout>
</template>
<script lang="ts" setup>
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
definePageMeta({
layout: "client"
});
</script>
<style>
</style>

82
pages/me/friends.vue Normal file
View file

@ -0,0 +1,82 @@
<template>
<NuxtLayout name="client">
<DirectMessagesSidebar />
<div id="friends-page-content">
<div id="navigation-bar">
<NuxtLink class="friends-sub-page-button" @click.prevent="updateHash('all')">All Friends</NuxtLink>
<NuxtLink class="friends-sub-page-button" @click.prevent="updateHash('online')">Online</NuxtLink>
<NuxtLink class="friends-sub-page-button" @click.prevent="updateHash('pending')">Pending</NuxtLink>
<NuxtLink class="friends-sub-page-button friend-primary-button" @click.prevent="updateHash('addfriend')">Add Friend</NuxtLink>
</div>
<div>
<AddFriend v-if="windowHash == '#addfriend'"></AddFriend>
<FriendsList v-else :variant="windowHash"></FriendsList>
</div>
</div>
</NuxtLayout>
</template>
<script lang="ts" setup>
import AddFriend from '~/components/Me/AddFriend.vue';
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
import FriendsList from '~/components/Me/FriendsList.vue';
let windowHash = ref(window.location.hash)
function updateHash(newHash: string) {
window.location.hash = newHash
windowHash.value = `#${newHash}`;
}
</script>
<style>
#friends-page-content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin: .75em;
}
#navigation-bar {
display: flex;
align-items: left;
text-align: left;
flex-direction: row;
gap: .5em;
}
.friends-sub-page-button {
text-decoration: none;
color: inherit;
font-weight: medium;
padding: 0.2em .5em;
flex-shrink: 0;
border-radius: 1em;
background-color: var(--accent-color);
display: inline-block;
text-align: center;
align-content: center;
cursor: pointer;
transition: background-color 300ms;
}
.friends-sub-page-button:hover {
background-color: var(--accent-highlighted-color);
}
.friend-primary-button {
background-color: var(--primary-color);
}
.friend-primary-button:hover {
background-color: var(--primary-highlighted-color);
}
</style>

View file

@ -1,23 +1,12 @@
<template>
<NuxtLayout>
<div id="left-bar">
</div>
<div id="middle-bar">
<h1>
Welcome to gorb :3
</h1>
<p>
Click on a guild to the left to view a guild.
<br>
Or click the button in the bottom left to join a guild.
</p>
</div>
<div id="right-bar">
</div>
<NuxtLayout name="client">
<DirectMessagesSidebar />
</NuxtLayout>
</template>
<script lang="ts" setup>
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
definePageMeta({
layout: "client"

View file

@ -18,7 +18,7 @@
</h3>
</div>
<div id="channels-list">
<Channel v-for="channel of channels" :name="channel.name"
<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>
@ -33,6 +33,7 @@
</template>
<script lang="ts" setup>
import ChannelEntry from "~/components/Guild/ChannelEntry.vue";
const route = useRoute();
@ -46,7 +47,6 @@ const channel = ref<ChannelResponse | undefined>();
const showInvitePopup = ref(false);
import UserPopup from "~/components/UserPopup.vue";
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
@ -91,7 +91,8 @@ function handleMemberClick(member: GuildMemberResponse) {
}
#members-container {
width: 15rem;
min-width: 15rem;
max-width: 15rem;
border-left: 1px solid var(--padding-color);
background: var(--optional-member-list-background);
}

View file

@ -8,7 +8,7 @@
<Icon class="back-button" size="2em" name="lucide:circle-arrow-left" alt="Back"></Icon>
</span>
</p>
<span class="spacer"></span>
<VerticalSpacer />
<!-- categories and dynamic settings pages -->
<div v-for="category in categories" :key="category.displayName">
@ -17,13 +17,13 @@
:class="{ 'sidebar-focus': selectedPage === page.displayName }">
{{ page.displayName }}
</li>
<span class="spacer"></span>
<verticalSpacer />
</div>
<p>
<Button text="Log Out" :callback=logout variant="scary"></Button>
</p>
<span class="spacer"></span>
<verticalSpacer />
<p id="links-and-socials">
<NuxtLink href="https://git.gorb.app/gorb/frontend" title="Source"><Icon name="lucide:git-branch-plus" /></NuxtLink>
@ -46,6 +46,9 @@
<script lang="ts" setup>
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
import Button from '~/components/UserInterface/Button.vue';
const { logout } = useAuth()
const appConfig = useRuntimeConfig()
@ -196,13 +199,6 @@ onMounted(() => {
margin-right: 0.2em;
}
.spacer {
height: 0.2dvh;
display: block;
margin: 0.8dvh 1dvw;
background-color: var(--padding-color);
}
/* applies to child pages too */
:deep(.subtitle) {
display: block;