Compare commits
34 commits
87a5b99e50
...
eb49450756
Author | SHA1 | Date | |
---|---|---|---|
eb49450756 | |||
de6c9bb7eb | |||
562409b660 | |||
963da24046 | |||
1d1cfa0af2 | |||
b0e56e1a06 | |||
457405186a | |||
3fc8933b1e | |||
0d6786ffe9 | |||
b731228fb8 | |||
f5457f9965 | |||
83464c8f13 | |||
890fbebbe9 | |||
79aa61cb81 | |||
010964f188 | |||
9256f9326b | |||
ba8abee256 | |||
f226ba2364 | |||
6752b44e95 | |||
d9aef4eb3a | |||
edb6c01b52 | |||
4e2e61d4dc | |||
0562127e4a | |||
34976b4f50 | |||
c9bea94ef8 | |||
a3feb07e73 | |||
7b62d352f8 | |||
8e69dc805e | |||
59000709fe | |||
15e5a21277 | |||
5dbf21b0ab | |||
8a9ecaa2e2 | |||
b1a3ce9b00 | |||
a90f062181 |
39 changed files with 513 additions and 139 deletions
63
components/Me/AddFriend.vue
Normal file
63
components/Me/AddFriend.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div style="text-align: left;">
|
||||
<h3>Add a Friend</h3>
|
||||
Enter a friend's Gorb username 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>
|
41
components/Me/DirectMessagesSidebar.vue
Normal file
41
components/Me/DirectMessagesSidebar.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div id="middle-left-column">
|
||||
<div id="friend-sidebar">
|
||||
<div>
|
||||
<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()
|
||||
</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>
|
58
components/Me/FriendsList.vue
Normal file
58
components/Me/FriendsList.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<input id="search-friend-bar" placeholder="search"/>
|
||||
|
||||
<!-- we aren't checking for the "all" variant, since this is the default and fallback one -->
|
||||
|
||||
<p v-if="props.variant === 'online'" style="text-align: left;">Online – 0</p>
|
||||
<p v-else-if="props.variant === 'pending'" style="text-align: left;">Friend Requests – 0</p>
|
||||
<p v-else style="text-align: left;">Friends – {{ friends?.length || 0 }}</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
|
||||
}>();
|
||||
</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>
|
|
@ -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
|
|
@ -2,6 +2,36 @@
|
|||
<div v-if="props.type == 'normal' || props.replyMessage" ref="messageElement" @contextmenu="createContextMenu($event, menuItems)" :id="props.last ? 'last-message' : undefined"
|
||||
class="message normal-message" :class="{ 'mentioned': (props.replyMessage || props.isMentioned) && props.message.user.uuid != props.me.uuid && props.replyMessage?.user.uuid == props.me.uuid }" :data-message-id="props.messageId"
|
||||
:editing.sync="props.editing" :replying-to.sync="props.replyingTo">
|
||||
<div v-if="props.replyMessage" class="message-reply-svg">
|
||||
<svg
|
||||
width="1.5em"
|
||||
height="1.5em"
|
||||
viewBox="0 0 151.14355 87.562065"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
style="overflow: visible;">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(40,-35)">
|
||||
<g
|
||||
id="g3"
|
||||
transform="translate(-35,-20)">
|
||||
<path
|
||||
style="stroke:var(--reply-text-color);stroke-width:8;stroke-opacity:1"
|
||||
d="m 120.02168,87.850978 100.76157,2.4e-5"
|
||||
id="path3-5" />
|
||||
<path
|
||||
style="stroke:var(--reply-text-color);stroke-width:8;stroke-opacity:1"
|
||||
d="M 69.899501,174.963 120.2803,87.700931"
|
||||
id="path3-5-2" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<MessageReply v-if="props.replyMessage" :author="props.replyMessage.user.display_name || props.replyMessage.user.username" :text="props.replyMessage?.message"
|
||||
:id="props.message.uuid" :reply-id="props.replyMessage.uuid" max-width="reply" />
|
||||
<div class="left-column">
|
||||
|
@ -16,7 +46,8 @@
|
|||
<span class="message-date" :title="date.toString()">
|
||||
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
||||
<span v-else-if="getDayDifference(date, currentDate) > 1 ">{{ date.toLocaleDateString(undefined) }},</span>
|
||||
{{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
|
||||
|
||||
{{ date.toLocaleTimeString(undefined, { hour12: props.format=="12", timeStyle: "short" }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="message-text" v-html="sanitized" tabindex="0"></div>
|
||||
|
@ -211,6 +242,11 @@ function getDayDifference(date1: Date, date2: Date) {
|
|||
background-color: rgba(90, 255, 200, 0.233);
|
||||
}
|
||||
|
||||
.message-reply-svg {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style module>
|
||||
|
|
|
@ -49,7 +49,7 @@ const me = await fetchWithApi("/me") as UserResponse;
|
|||
const messageTimestamps = ref<Record<string, number>>({});
|
||||
const messagesType = ref<Record<string, "normal" | "grouped">>({});
|
||||
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference
|
||||
const timeFormat = settingLoad("timeFormat") ?? "24"
|
||||
const timeFormat = getPreferredTimeFormat()
|
||||
|
||||
const messagesRes: MessageResponse[] | undefined = await fetchWithApi(
|
||||
`${props.channelUrl}/messages`,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :id="props.maxWidth == 'full' ? 'message-reply' : undefined" :class="{ 'message-reply-preview' : props.maxWidth == 'reply' }"
|
||||
:data-message-id="props.id" @click="scrollToReply">
|
||||
<p id="reply-text">Replying to <span id="reply-author-field">{{ props.author }}:</span> <span v-html="sanitized"></span></p>
|
||||
<span id="reply-text">Replying to <span id="reply-author-field">{{ props.author }}:</span> <span v-html="sanitized"></span></span>
|
||||
<!-- <span id="message-reply-cancel"><Icon name="lucide:x" /></span> -->
|
||||
</div>
|
||||
</template>
|
||||
|
@ -61,7 +61,6 @@ function scrollToReply(e: MouseEvent) {
|
|||
#message-reply, .message-reply-preview {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--padding-color);
|
||||
margin-bottom: .5rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
@ -72,16 +71,17 @@ function scrollToReply(e: MouseEvent) {
|
|||
}
|
||||
|
||||
.message-reply-preview {
|
||||
width: 30%;
|
||||
margin-left: .5dvw;
|
||||
}
|
||||
|
||||
#reply-text {
|
||||
color: rgb(150, 150, 150);
|
||||
color: var(--reply-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0;
|
||||
margin-top: .2rem;
|
||||
border-bottom: 1px solid var(--padding-color);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<div class="radio-button-container" ref="radioButtonContainer">
|
||||
<span v-for="index in incidies" :key="index"
|
||||
class="radio-button" @click="onClick(index)">
|
||||
{{ textStrings[index] }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const radioButtonContainer = ref<HTMLDivElement>()
|
||||
|
||||
const props = defineProps<{
|
||||
textStrings: string[],
|
||||
buttonCount: number,
|
||||
defaultButtonIndex: number,
|
||||
callback: CallableFunction,
|
||||
}>();
|
||||
|
||||
// makes an array from 0 to buttonCount - 1
|
||||
const incidies = Array.from({ length: props.buttonCount }, (_, i) => i)
|
||||
|
||||
function onClick(clickedIndex: number) {
|
||||
// remove selected-radio-button class from all buttons except the clicked one
|
||||
if (radioButtonContainer.value) {
|
||||
const children = radioButtonContainer.value.children
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
children.item(i)?.classList.remove("selected-radio-button")
|
||||
}
|
||||
|
||||
children.item(clickedIndex)?.classList.add("selected-radio-button")
|
||||
}
|
||||
|
||||
props.callback(clickedIndex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.radio-button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 1em;
|
||||
background-color: unset;
|
||||
color: var(--text-color);
|
||||
|
||||
padding: 0.4em 0.75em;
|
||||
margin: 0.4em 0em;
|
||||
font-size: 1.1em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.radio-button:hover {
|
||||
background-color: var(--secondary-highlighted-color);
|
||||
}
|
||||
|
||||
.selected-radio-button {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.selected-radio-button:hover {
|
||||
background-color: var(--primary-highlighted-color);
|
||||
}
|
||||
|
||||
</style>
|
|
@ -23,18 +23,21 @@
|
|||
|
||||
<p class="subtitle">TIME FORMAT</p>
|
||||
<div class="icons">
|
||||
<RadioButtons :button-count="3" :text-strings="['Auto', '12-hour', '24-hour']"
|
||||
default-button-index="0" :callback="onTimeFormatClicked"></RadioButtons>
|
||||
<RadioButtons :button-count="3" :text-strings="timeFormatTextStrings"
|
||||
:default-button-index="settingLoad('timeFormat')?.index ?? 0" :callback="onTimeFormatClicked"></RadioButtons>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RadioButtons from '~/components/UserInterface/RadioButtons.vue';
|
||||
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
|
||||
import settingSave from '~/utils/settingSave';
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const defaultThemes = runtimeConfig.public.defaultThemes
|
||||
const baseURL = runtimeConfig.app.baseURL;
|
||||
const timeFormatTextStrings = ["Auto", "12-Hour", "24-Hour"]
|
||||
let themeLinkElement: HTMLLinkElement | null = null;
|
||||
|
||||
const themes: Array<Theme> = []
|
||||
|
@ -75,9 +78,8 @@ async function fetchThemes() {
|
|||
|
||||
await fetchThemes()
|
||||
|
||||
|
||||
async function onTimeFormatClicked(index: number) {
|
||||
console.log(index)
|
||||
settingSave("timeFormat", {index, timeFormat: timeFormatTextStrings[index].toLowerCase()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Button from '~/components/Button.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -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;
|
||||
|
|
40
components/User/UserEntry.vue
Normal file
40
components/User/UserEntry.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<NuxtLink class="user-item" :href="`/me/${user.uuid}`" tabindex="0">
|
||||
<img v-if="props.user.avatar" class="user-avatar" :src="props.user.avatar" :alt="props.user.display_name ?? props.user.username" />
|
||||
<Icon v-else class="user-avatar" name="lucide:user" />
|
||||
<span class="user-display-name">{{ props.user.display_name || props.user.username }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserResponse } from '~/types/interfaces';
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserResponse
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
gap: .5em;
|
||||
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background-color: #00000020
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 2.3em;
|
||||
height: 2.3em;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
|
@ -1,20 +0,0 @@
|
|||
<template>
|
||||
<div id="user-panel">
|
||||
HELLO!!
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserResponse } from '~/types/interfaces';
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserResponse,
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#user-panel {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
111
components/UserInterface/RadioButtons.vue
Normal file
111
components/UserInterface/RadioButtons.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div class="radio-buttons-container" ref="radioButtonsContainer">
|
||||
<div v-for="index in incidies" :key="index" class="radio-button" @click="onClick(index)">
|
||||
<span class="radio-button-radio"></span>
|
||||
<span class="radio-button-text">{{ textStrings[index] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const radioButtonsContainer = ref<HTMLDivElement>()
|
||||
|
||||
const props = defineProps<{
|
||||
textStrings: string[],
|
||||
buttonCount: number,
|
||||
defaultButtonIndex: number,
|
||||
callback: CallableFunction,
|
||||
}>();
|
||||
|
||||
// makes an array from 0 to buttonCount - 1
|
||||
const incidies = Array.from({ length: props.buttonCount }, (_, i) => i)
|
||||
|
||||
// select default selected button
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
if (props.defaultButtonIndex != undefined && radioButtonsContainer.value) {
|
||||
const children = radioButtonsContainer.value.children
|
||||
const defaultButton = children.item(props.defaultButtonIndex)
|
||||
defaultButton?.classList.add("selected-radio-button")
|
||||
defaultButton?.children.item(0)?.classList.add("selected-radio-button-radio")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function onClick(clickedIndex: number) {
|
||||
// remove selected-radio-button class from all buttons except the clicked one
|
||||
if (radioButtonsContainer.value) {
|
||||
const children = radioButtonsContainer.value.children
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
children.item(i)?.classList.remove("selected-radio-button")
|
||||
children.item(i)?.children.item(0)?.classList.remove("selected-radio-button-radio")
|
||||
}
|
||||
|
||||
children.item(clickedIndex)?.classList.add("selected-radio-button")
|
||||
children.item(clickedIndex)?.children.item(0)?.classList.add("selected-radio-button-radio")
|
||||
}
|
||||
|
||||
props.callback(clickedIndex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.radio-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border-radius: .66em;
|
||||
background-color: unset;
|
||||
color: var(--text-color);
|
||||
|
||||
padding: 0.4em 0.75em;
|
||||
margin: 0.4em 0em;
|
||||
font-size: 1.1em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.radio-button:hover {
|
||||
background-color: var(--secondary-highlighted-color);
|
||||
}
|
||||
|
||||
.selected-radio-button {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.selected-radio-button:hover {
|
||||
background-color: var(--accent-highlighted-color);
|
||||
}
|
||||
|
||||
.radio-button-radio, .selected-radio-button-radio {
|
||||
position: relative;
|
||||
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.radio-button-radio {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
border: .15em solid var(--primary-color);
|
||||
}
|
||||
|
||||
.selected-radio-button-radio {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
border: 0.15em solid var(--primary-color);
|
||||
background-color: var(--primary-highlighted-color);
|
||||
}
|
||||
|
||||
.radio-button-text {
|
||||
margin-left: .5em;
|
||||
}
|
||||
</style>
|
12
components/UserInterface/VerticalSpacer.vue
Normal file
12
components/UserInterface/VerticalSpacer.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<span class="spacer"></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spacer {
|
||||
height: 0.2dvh;
|
||||
display: block;
|
||||
margin: 0.8dvh 0.2dvw;
|
||||
background-color: var(--padding-color);
|
||||
}
|
||||
</style>
|
|
@ -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> {
|
||||
return await fetchWithApi('/me/friends', { method: "POST", body: { username } });
|
||||
}
|
||||
|
||||
async function removeFriend(userId: string): Promise<void> {
|
||||
return 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,
|
||||
|
|
|
@ -106,7 +106,7 @@ export const useAuth = () => {
|
|||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
clearAuth,
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
<div id = "page-content">
|
||||
<div id="left-column">
|
||||
<NuxtLink id="home-button" href="/">
|
||||
<NuxtLink id="home-button" href="/me">
|
||||
<img class="sidebar-icon" src="/public/icon.svg"/>
|
||||
</NuxtLink>
|
||||
<div id="servers-list">
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
await navigateTo("/me/", { replace: true })
|
||||
|
||||
definePageMeta({
|
||||
layout: "client"
|
||||
|
|
15
pages/me/[userId].vue
Normal file
15
pages/me/[userId].vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
<MessageArea channel-url="channels/01970e8c-a09c-76a0-9c98-80a43364bea7"/> <!-- currently just links to the default channel -->
|
||||
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
56
pages/me/friends.vue
Normal file
56
pages/me/friends.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
<div :id="$style['page-content']">
|
||||
<div :id="$style['navigation-bar']">
|
||||
<Button :text="`All Friends – ${friends?.length}`" variant="neutral" :callback="() => updateFilter('all')" />
|
||||
<Button :text="`Online – ${0}`" variant="neutral" :callback="() => updateFilter('online')" />
|
||||
<Button :text="`Pending – ${0}`" variant="neutral" :callback="() => updateFilter('pending')" />
|
||||
<Button text="Add Friend" variant="normal" :callback="() => updateFilter('add')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AddFriend v-if="filter == 'add'"></AddFriend>
|
||||
<FriendsList v-else :variant="filter"></FriendsList>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
import Button from '~/components/UserInterface/Button.vue';
|
||||
import AddFriend from '~/components/Me/AddFriend.vue';
|
||||
import FriendsList from '~/components/Me/FriendsList.vue';
|
||||
|
||||
const { fetchFriends } = useApi();
|
||||
|
||||
let filter = ref("all");
|
||||
|
||||
const friends = await fetchFriends()
|
||||
|
||||
function updateFilter(newFilter: string) {
|
||||
filter.value = newFilter;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
#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;
|
||||
}
|
||||
</style>
|
13
pages/me/index.vue
Normal file
13
pages/me/index.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<NuxtLayout name="client">
|
||||
<DirectMessagesSidebar />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DirectMessagesSidebar from '~/components/Me/DirectMessagesSidebar.vue';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--text-color: #f0e5e0;
|
||||
--secondary-text-color: #e8e0db;
|
||||
--reply-text-color: #969696;
|
||||
|
||||
--chat-background-color: #2f2e2d;
|
||||
--chat-highlighted-background-color: #3f3b38;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--text-color: #f7eee8;
|
||||
--secondary-text-color: #f0e8e4;
|
||||
--reply-text-color: #969696;
|
||||
|
||||
--chat-background-color: #1f1e1d;
|
||||
--chat-highlighted-background-color: #2f2b28;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
:root {
|
||||
--text-color: #161518;
|
||||
--secondary-text-color: #2b2930;
|
||||
--reply-text-color: #969696;
|
||||
|
||||
--chat-background-color: #80808000;
|
||||
--chat-highlighted-background-color: #ffffff20;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--text-color: #170f08;
|
||||
--secondary-text-color: #2f2b28;
|
||||
--reply-text-color: #969696;
|
||||
|
||||
--chat-background-color: #f0ebe8;
|
||||
--chat-highlighted-background-color: #e8e4e0;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--text-color: #161518;
|
||||
--secondary-text-color: #2b2930;
|
||||
--reply-text-color: #969696;
|
||||
|
||||
--chat-background-color: #80808000;
|
||||
--chat-highlighted-background-color: #ffffff20;
|
||||
|
|
|
@ -62,7 +62,8 @@ export interface UserResponse {
|
|||
pronouns: string | null,
|
||||
about: string | null,
|
||||
email?: string,
|
||||
email_verified?: boolean
|
||||
email_verified?: boolean,
|
||||
friends_since: string | null,
|
||||
}
|
||||
|
||||
export interface StatsResponse {
|
||||
|
|
|
@ -18,7 +18,7 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
|
|||
return;
|
||||
}
|
||||
console.log("path:", path)
|
||||
const { revoke, refresh } = useAuth();
|
||||
const { clearAuth, refresh } = useAuth();
|
||||
|
||||
let headers: HeadersInit = {};
|
||||
|
||||
|
@ -61,8 +61,7 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
|
|||
if (error?.response?.status === 401) {
|
||||
console.log("Refresh returned 401");
|
||||
reauthFailed = true;
|
||||
console.log("Revoking");
|
||||
await revoke();
|
||||
await clearAuth()
|
||||
console.log("Redirecting to login");
|
||||
await navigateTo("/login");
|
||||
console.log("redirected");
|
||||
|
|
11
utils/getPreferredTimeFormat.ts
Normal file
11
utils/getPreferredTimeFormat.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default (): "12" | "24" => {
|
||||
const format = settingLoad("timeFormat").timeFormat ?? "auto"
|
||||
|
||||
if (format == "12-hour") {
|
||||
return "12"
|
||||
} else if (format == "24-hour") {
|
||||
return "24"
|
||||
}
|
||||
|
||||
return "24"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue