a bunch of frontend layout adjustments, and fixed message duplication by adding uuids

This commit is contained in:
2025-12-28 22:50:16 +01:00
parent dd293c8e3d
commit 24a460e6e2
8 changed files with 306 additions and 99 deletions

View File

@@ -4,116 +4,49 @@
<router-view />
</main>
<nav id="bottom-nav">
<router-link to="/" class="nav-item" aria-label="Home">
<i class="fa-solid fa-message"></i>
</router-link>
<router-link to="/friendlist" class="nav-item" aria-label="Friends">
<i class="fa-solid fa-user-group"></i>
</router-link>
<button class="nav-item logout" @click="logout" aria-label="Logout">
<i class="fa-solid fa-right-from-bracket"></i>
</button>
</nav>
<footer v-if="!$route.meta.hideNavbar">
<Navbar />
</footer>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { logout as authLogout } from './stores/auth.ts'
const router = useRouter()
function logout() {
authLogout()
router.push('/login')
}
import Navbar from './components/Navbar.vue';
</script>
<style scoped>
#page {
min-height: 100vh;
background: var(--bg);
height: 100dvh;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
#content {
width: 100%;
max-width: 1100px;
padding: 2rem;
padding-bottom: 120px;
flex: 1;
overflow-y: auto;
}
#bottom-nav {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
footer {
width: 100%;
display: flex;
gap: 28px;
padding: 14px 22px;
background: color-mix(in srgb, var(--panel) 85%, transparent);
border: 1px solid var(--border);
border-radius: 999px;
backdrop-filter: blur(10px);
z-index: 50;
}
.nav-item {
all: unset;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: var(--muted);
font-size: 1.25rem;
border-radius: 50%;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.nav-item:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
.nav-item:not(.router-link-active):hover {
color: var(--text);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
.router-link-active {
color: var(--accent);
}
.logout:hover {
color: rgba(255, 80, 80, 0.8);
padding-bottom: 24px;
background: var(--bg);
}
@media (max-width: 720px) {
#content {
padding: 1.2rem;
padding-bottom: 130px;
}
#bottom-nav {
bottom: 16px;
footer {
padding-bottom: 16px;
}
}
</style>

View File

@@ -13,6 +13,7 @@ html,
body,
#app {
height: 100%;
overflow-y: hidden;
}
:root {
@@ -63,9 +64,89 @@ button, .button {
cursor: pointer;
text-decoration: none;
margin: 5px;
outline: none;
}
button:hover, .button:hover {
background: var(--accent-hover);
}
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
aspect-ratio: 1;
flex-shrink: 0;
padding: 0;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
display: inline-grid;
place-content: center;
vertical-align: middle;
position: relative;
transition: all 0.15s ease-in-out;
}
input[type="checkbox"]:hover {
border-color: var(--accent);
}
input[type="checkbox"]:checked {
background: var(--accent);
border-color: var(--accent);
}
input[type="checkbox"]::before {
content: "";
width: 0.7rem;
height: 0.7rem;
transform: scale(0);
transition: 120ms transform ease-in-out;
background-color: #0b0d12;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--text);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
filter: brightness(1.2);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--text) transparent;
}
i {
color: var(--muted);
transition: color 0.2s ease;
}
i:hover {
color: var(--text);
}
.btn {
outline: none;
}

View File

@@ -66,8 +66,7 @@ async function initializeRoom() {
socket.onmessage = (event) => {
const msg: Message = JSON.parse(event.data);
const exists = messages.value.some(m => m.sent_at === msg.sent_at && m.sender === msg.sender);
if (!exists) {
if (!messages.value.some(m => m.uuid === msg.uuid)) {
messages.value.push(msg);
nextTick().then(scrollToBottom);
}

95
src/components/Navbar.vue Normal file
View File

@@ -0,0 +1,95 @@
<template>
<div>
<nav id="bottom-nav">
<router-link to="/rooms/none" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
<router-link to="/friendlist" class="nav-item">
<i class="fa-solid fa-user-group"></i>
</router-link>
<button class="nav-item logout" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
</button>
</nav>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { logout as authLogout } from '../stores/auth.ts'
const router = useRouter()
function logout() {
authLogout()
router.push('/login')
}
</script>
<style scoped>
#bottom-nav {
display: flex;
gap: 28px;
padding: 5px 22px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 100vh;
z-index: 50;
}
.nav-item {
all: unset;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 50%;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.nav-item i {
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:not(.router-link-active):hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:not(.router-link-active):hover i {
color: var(--text);
}
.router-link-active i {
color: var(--accent);
}
.nav-item.logout:hover i {
color: rgba(255, 80, 80, 0.8);
}
@media (max-width: 720px) {
#bottom-nav {
bottom: 16px;
}
}
</style>

View File

@@ -2,17 +2,17 @@
<div class="room-list">
<header class="rooms-header">
<h2>Rooms</h2>
<button class="create-btn" @click="showCreate = true">+</button>
<button class="create-btn" @click="showCreate = true"><i class="fa-solid fa-plus"></i></button>
</header>
<CreateRoomModal v-if="showCreate" @close="showCreate = false" @created="rooms.push($event)" />
<div class="scroll-area">
<router-link v-for="room in rooms" :key="room.uuid" :to="`/rooms/${room.uuid}`" class="room-item"
<router-link v-for="room in rooms" :key="room.uuid" :to="`/rooms/${room.uuid}`" class="btn room-item"
:class="{ active: route.params.uuid === room.uuid }">
<div class="room-info">
<span class="room-name">{{ room.name }}</span>
<span class="room-owner">{{ room.owner_name }}</span>
<span class="room-owner">by {{ room.owner_name }}</span>
</div>
</router-link>
</div>
@@ -55,6 +55,7 @@ onMounted(async () => {
.rooms-header h2 {
font-size: 1.1rem;
margin: 0;
margin-left: 38px;
}
.scroll-area {
@@ -80,11 +81,11 @@ onMounted(async () => {
.room-item.active {
background: var(--accent);
color: white;
color: rgba(0, 0, 0, 0.8);
}
.room-item.active .room-owner {
color: rgba(255, 255, 255, 0.7);
color: rgba(0, 0, 0, 1);
}
.room-info {
@@ -102,15 +103,21 @@ onMounted(async () => {
}
.create-btn {
margin: 0;
padding: 18px;
width: 28px;
height: 28px;
border-radius: 50%;
border-radius: var(--radius);
border: none;
background: var(--accent);
background: transparent;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.create-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
</style>

View File

@@ -1,9 +1,17 @@
<template>
<div class="chat-layout">
<aside class="sidebar">
<RoomList />
<button class="menu-toggle" :class="{ 'sidebar-closed': !isSidebarOpen }" @click="isSidebarOpen = !isSidebarOpen">
<i class="fa-solid fa-bars"></i>
</button>
<aside class="sidebar" :class="{ 'is-open': isSidebarOpen }">
<div class="sidebar-content">
<RoomList />
</div>
</aside>
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
<main class="chat-window-container">
<ChatWindow :uuid="uuid" />
</main>
@@ -11,15 +19,20 @@
</template>
<script setup lang="ts">
defineProps<{ uuid: string }>();
import { ref } from 'vue';
import RoomList from "../components/RoomList.vue";
import ChatWindow from "../components/ChatWindow.vue";
defineProps<{ uuid: string }>();
const isSidebarOpen = ref(true);
</script>
<style scoped>
.chat-layout {
position: relative;
display: flex;
height: 80vh;
height: 100%;
width: 100%;
max-width: 1200px;
background: var(--panel);
@@ -33,6 +46,20 @@ import ChatWindow from "../components/ChatWindow.vue";
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
background: var(--panel);
transition: width 0.3s ease, transform 0.3s ease;
z-index: 20;
overflow: hidden;
}
.sidebar:not(.is-open) {
width: 0;
border-right: none;
}
.sidebar-content {
width: 300px;
height: 100%;
}
.chat-window-container {
@@ -40,11 +67,65 @@ import ChatWindow from "../components/ChatWindow.vue";
display: flex;
flex-direction: column;
min-width: 0;
transition: all 0.3s ease;
}
.menu-toggle {
position: absolute;
top: 24px;
left: 15px;
z-index: 30;
background: var(--panel);
border-radius: 4px;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: left 0.3s ease;
}
.menu-toggle.sidebar-closed {
left: 15px;
}
.menu-toggle i {
font-size: 1.6rem;
}
@media (max-width: 720px) {
.sidebar {
display: none;
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 280px;
transform: translateX(-100%);
}
.sidebar.is-open {
transform: translateX(0);
width: 280px;
}
.sidebar:not(.is-open) {
width: 280px;
transform: translateX(-100%);
}
.sidebar-content {
width: 280px;
}
.sidebar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 15;
}
.chat-window-container {
padding-top: 50px;
}
}
</style>

View File

@@ -9,8 +9,18 @@ const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/rooms/none' },
{ path: '/login', component: LoginPage },
{ path: '/rooms/:uuid', component: ChatPage, props: true },
{
path: '/login',
name: 'login',
component: LoginPage,
meta: { hideNavbar: true }
},
{
path: '/rooms/:uuid',
name: 'chat',
component: ChatPage,
props: true
},
{ path: '/friendlist', component: FriendListPage }
],
})

View File

@@ -11,6 +11,7 @@ export interface Room {
}
export interface Message {
uuid: string
sender: string
message_type: 'text'
content: string