added room actions: leave and delete, and improved connection error handling

This commit is contained in:
2026-01-17 07:09:52 +01:00
parent e30631be60
commit d325511d0e
17 changed files with 874 additions and 350 deletions

View File

@@ -1,8 +1,8 @@
{
"name": "frangipane-client",
"private": true,
"version": "0.1.0",
"backendVersion": "1.0.2",
"version": "1.0.0",
"backendVersion": "1.0.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "frangipane",
"version": "0.1.0",
"version": "1.0.0",
"identifier": "com.strawberries.frangipane",
"build": {
"beforeDevCommand": "yarn dev",

View File

@@ -1,5 +1,5 @@
import { apiFetch } from './client'
import type { Room, RoomInvite } from '../types'
import { UserProfile, type Room, type RoomInvite } from '../types'
export function fetchRooms() {
return apiFetch<Room[]>(`/rooms`)
@@ -40,3 +40,26 @@ export function declineRoomInvite(senderUuid: string, roomUuid: string) {
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
})
}
export function leaveRoom(roomUuid: string) {
return apiFetch<void>(`/rooms/${roomUuid}/leave`, {
method: 'POST'
})
}
export function deleteRoom(roomUuid: string) {
return apiFetch<void>(`/rooms/${roomUuid}/delete`, {
method: 'DELETE'
})
}
export function transferOwnership(roomUuid: string, newOwnerUuid: string) {
return apiFetch<void>('/rooms/transfer-ownership', {
method: 'POST',
body: JSON.stringify({ room_uuid: roomUuid, new_owner_uuid: newOwnerUuid }),
})
}
export function listMembers(roomUuid: string) {
return apiFetch<UserProfile[]>(`/rooms/${roomUuid}/members`)
}

View File

@@ -24,6 +24,7 @@ body,
--muted: #9aa0aa;
--accent: #f27aa3;
--accent-hover: #ff91b3;
--accent-second: #96CDFB;
--border: #2a2f3b;
--error: #ff5050;
--radius: 8px;
@@ -174,6 +175,15 @@ i:hover {
margin: 30px;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border);
flex-shrink: 0;
}
@media (hover: hover) {
i:hover {
color: var(--text);

View File

@@ -1,6 +1,10 @@
<template>
<InvitePeopleModal v-if="showInviteModal && isSocketConnected" :room_uuid="props.uuid"
@close="showInviteModal = false" />
@close="showInviteModal = false" @room-changed="handleRoomChanged" />
<RoomDetailsModal v-if="showDetailsModal && isSocketConnected" :roomUuid="props.uuid"
:roomName="currentRoom?.name || 'Unknown room'" :isGlobal="currentRoom?.global || false"
@close="showDetailsModal = false" @room-changed="handleRoomChanged" />
<div v-if="uuid === 'none'" class="no-room">
<div class="empty-state">
@@ -10,15 +14,30 @@
</div>
<div v-else class="chat-container">
<h2 class="room-name">{{ currentRoom?.name }}</h2>
<button class="room-name" @click="showDetailsModal = true">{{ currentRoom?.name }}</button>
<div class="messages-container" ref="messageListRef" @scroll="handleScroll">
<MessageList v-if="messages.length > 0 || isSocketConnected" :messages="messages" />
</div>
<p v-if="messages.length === 0" class="wait-msg">{{ $t('chat-connecting') }}</p>
<div v-if="messages.length == 0" class="center-status-container">
<p v-if="isInitialLoad" class="wait-msg">{{ $t('chat-connecting') }}</p>
<div class="input-container">
<div v-else-if="connectionError" class="wait-msg">
<p>{{ $t('chat-connecting-failed') }}</p>
<button class="retry-btn" @click="retryConnection">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div v-else-if="!connectionError && isSocketConnected" class="empty-room-state">
<i class="fa-regular fa-paper-plane"></i>
<p>{{ $t('chat-no-messages') }}</p>
</div>
</div>
<div v-if="isSocketConnected" class="input-container">
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
:title="$t('chat-invite-title')">
<i class="fa-solid fa-users"></i>
@@ -26,9 +45,6 @@
<div v-if="connectionError" class="connection-error">
<p>{{ connectionError }}</p>
<button class="retry-btn" @click="retryConnection">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<MessageInput v-if="isSocketConnected" ref="messageInputRef" @send="onSend" />
@@ -45,6 +61,7 @@ import { apiFetch } from '../api/client';
import MessageList from "./MessageList.vue";
import MessageInput from "./MessageInput.vue";
import InvitePeopleModal from './InvitePeopleModal.vue';
import RoomDetailsModal from "./RoomDetailsModal.vue";
import WebSocket from '@tauri-apps/plugin-websocket';
import { getAuthData } from "../store.ts";
import { fetchRoomInfo } from "../api/rooms.ts";
@@ -53,10 +70,11 @@ import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const emit = defineEmits<{
// (e: 'send', content: string): void
(e: 'notification', roomUuid: string): void
(e: 'room-action'): void
}>();
const props = defineProps<{ uuid: string }>();
// UI State
@@ -71,6 +89,8 @@ const connectionError = ref<string | null>(null);
const isLoadingMore = ref(false);
const hasMore = ref(true);
const showInviteModal = ref(false);
const showDetailsModal = ref(false);
const isInitialLoad = ref(false);
// WebSocket State
let socket: WebSocket | null = null;
@@ -82,6 +102,11 @@ const isOwner = computed(() => {
return currentUser.value.uuid === currentRoom.value.owner_uuid;
});
const handleRoomChanged = () => {
showDetailsModal.value = false;
emit('room-action');
};
onMounted(async () => {
await connectGlobalWebSocket();
@@ -109,12 +134,15 @@ async function retryConnection() {
}
async function loadRoomData() {
isInitialLoad.value = true;
messages.value = [];
currentRoom.value = null;
hasMore.value = true;
connectionError.value = null;
if (props.uuid === 'none') return;
if (props.uuid === 'none') {
isInitialLoad.value = false;
return;
}
try {
const [msgs, roomInfo, auth] = await Promise.all([
@@ -132,6 +160,8 @@ async function loadRoomData() {
scrollToBottom();
} catch (err) {
console.error("Room data load failed:", err);
} finally {
isInitialLoad.value = false;
}
}
@@ -177,7 +207,7 @@ async function connectGlobalWebSocket() {
} catch (err) {
console.error("WS Connect failed:", err);
isSocketConnected.value = false;
connectionError.value = "Live chat disconnected.";
connectionError.value = $t('shared-error');
}
}
@@ -314,6 +344,14 @@ async function onSend(content: string) {
}
.room-name {
border-radius: var(--radius);
border: none;
background: transparent;
color: white;
cursor: pointer;
font-size: 1.6rem;
font-weight: bold;
margin: 15px 0;
text-align: center;
}
@@ -371,4 +409,44 @@ async function onSend(content: string) {
margin-bottom: 1rem;
opacity: 0.3;
}
.center-status-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
pointer-events: none;
text-align: center;
width: 100%;
}
.wait-msg {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.6rem;
margin: 0;
color: var(--muted);
font-size: 1.1rem;
}
.empty-room-state {
color: var(--muted);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
opacity: 0.7;
}
.empty-room-state i {
font-size: 2rem;
}
.empty-room-state p {
margin: 0;
font-size: 1rem;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="backdrop" @click.self="emit('no')">
<div class="modal">
<h2>{{ title }}</h2>
<p v-if="message">{{ message }}</p>
<div class="actions">
<button @click="emit('no')" class="secondary">
{{ cancelLabel || $t('shared-cancel') || 'Cancel' }}
</button>
<button @click="emit('yes')" :class="['btn', confirmButtonClass]">
{{ confirmLabel || $t('shared-confirm') || 'Confirm' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
confirmButtonClass?: string;
}>();
const emit = defineEmits(['yes', 'no']);
</script>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.modal h2 {
margin: 0;
font-size: 1.25rem;
}
.modal p {
margin: 0;
color: var(--text-muted);
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.btn {
cursor: pointer;
padding: 8px 16px;
border-radius: var(--radius);
border: 1px solid var(--accent);
background: transparent;
color: var(--text);
}
.secondary {
cursor: pointer;
padding: 8px 16px;
border-radius: var(--radius);
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn-danger {
border-color: var(--error);
color: var(--error);
}
.btn-danger:hover {
background-color: var(--error);
color: white;
}
</style>

View File

@@ -2,7 +2,7 @@
<ul>
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
<div class="sender-info">
<img :src="getAvatarUrl(m.sender_uuid)" @error="handleAvatarError" class="sender-avatar" />
<img :src="getAvatarUrl(m.sender_uuid)" @error="handleAvatarError" class="avatar" />
<div class="sender">{{ m.sender }}</div>
<span class="timestamp">{{ m.sent_at }}</span>
</div>
@@ -74,15 +74,6 @@ ul {
padding: 5px 18px;
}
.sender-avatar {
width: 35px;
height: 35px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border);
flex-shrink: 0;
}
.message.is-me {
align-self: flex-end;
align-items: flex-end;

View File

@@ -0,0 +1,280 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<div class="modal">
<h2>{{ roomName }}</h2>
<h3 v-if="!isGlobal">Members:</h3>
<ul v-if="!isGlobal" class="member-list">
<li v-for="user in users" :key="user.uuid || user.username" class="member-item">
<img :src="getAvatarUrl(user.uuid)" @error="handleAvatarError" class="avatar" alt="avatar" />
<span class="member-name">{{ user.username }}</span>
</li>
</ul>
<p v-else class="global-tag">
<i class="fa-solid fa-earth"></i>
{{ $t('chat-room-global') }}
</p>
<div class="buttons">
<button v-if="!isGlobal && !isOwner" class="btn" @click="requestLeave" :disabled="isLoading">
{{ $t('chat-room-actions-leave') }}
</button>
<button v-if="isOwner" class="btn delete-btn" @click="requestDelete" :disabled="isLoading">
{{ $t('chat-room-actions-delete') }}
</button>
<!-- <button class="btn ownership-btn">{{ $t('chat-room-actions-ownership') }}</button> -->
</div>
<div class="actions">
<button @click="emit('close')" class="secondary">
{{ $t('shared-close') }}
</button>
</div>
</div>
</div>
<ConfirmModal v-if="modalState.visible" :title="modalState.title" :message="modalState.message"
:confirm-label="modalState.confirmLabel" :confirm-button-class="modalState.isDanger ? 'btn-danger' : ''"
@yes="handleConfirmAction" @no="closeConfirmModal" />
</template>
<script setup lang="ts">
import { computed, onMounted, ref, reactive } from 'vue';
import { UserProfile } from '../types';
import { deleteRoom, leaveRoom, listMembers } from '../api/rooms';
import { getAuthData, getAvatarUrl } from '../store.ts';
import defaultAvatar from '../assets/default-avatar.png';
import ConfirmModal from './ConfirmModal.vue';
import { useFluent } from 'fluent-vue';
const { $t } = useFluent()
const props = defineProps<{ roomUuid: string, roomName: string, isGlobal: boolean }>()
const emit = defineEmits(['close', 'room-changed']);
const users = ref<UserProfile[]>([]);
const isLoading = ref(false);
const currentUserUuid = ref<string>('');
type ActionType = 'leave' | 'delete' | null;
const modalState = reactive({
visible: false,
type: null as ActionType,
title: '',
message: '',
confirmLabel: '',
isDanger: false
});
const requestLeave = () => {
modalState.type = 'leave';
modalState.title = $t('chat-room-actions-leave');
modalState.message = $t('chat-room-actions-leave-confirm');
modalState.confirmLabel = $t('shared-leave');
modalState.isDanger = false;
modalState.visible = true;
};
const requestDelete = () => {
modalState.type = 'delete';
modalState.title = $t('chat-room-actions-delete');
modalState.message = $t('chat-room-actions-delete-confirm');
modalState.confirmLabel = $t('shared-delete');
modalState.isDanger = true;
modalState.visible = true;
};
const closeConfirmModal = () => {
modalState.visible = false;
modalState.type = null;
};
const handleConfirmAction = async () => {
const actionType = modalState.type;
closeConfirmModal();
isLoading.value = true;
try {
if (actionType === 'leave') {
await leaveRoom(props.roomUuid);
emit('room-changed');
emit('close');
} else if (actionType === 'delete') {
await deleteRoom(props.roomUuid);
emit('room-changed');
emit('close');
}
} catch (error) {
console.error(`Failed to ${actionType} room:`, error);
} finally {
isLoading.value = false;
}
};
const isOwner = computed(() => {
return true; // Logic placeholder
});
onMounted(async () => {
const auth = await getAuthData();
currentUserUuid.value = auth.user?.uuid || 'undefined';
if (!props.isGlobal) {
users.value = await listMembers(props.roomUuid);
}
});
const handleAvatarError = (event: Event) => {
const img = event.target as HTMLImageElement;
img.src = defaultAvatar;
};
</script>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
}
.modal {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
padding: 8px 16px;
border-radius: var(--radius);
cursor: pointer;
}
.member-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
}
.member-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.5rem;
}
.avatar {
width: 48px;
height: 48px;
}
.member-name {
font-size: 0.85rem;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-tag {
width: 100%;
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.6rem;
}
.buttons {
width: 100%;
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.6rem;
margin-top: 22px;
}
.btn {
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
font-size: 0.9rem;
color: var(--text);
background-color: transparent;
border-radius: var(--radius);
width: fit-content;
border: 1px solid var(--accent);
transition: all 0.2s ease;
}
.btn:hover {
color: var(--accent)
}
.btn:hover i {
color: var(--accent)
}
.btn i {
font-size: 1.25rem;
}
.delete-btn {
border: 1px solid var(--error);
}
.delete-btn:hover {
color: var(--error)
}
.delete-btn:hover i {
color: var(--error)
}
.ownership-btn {
border: 1px solid var(--accent-second);
}
.ownership-btn:hover {
color: var(--accent-second)
}
.ownership-btn:hover i {
color: var(--accent-second)
}
</style>

View File

@@ -11,14 +11,18 @@
</button>
</header>
<div class="wait-container" v-if="!rooms || rooms.length === 0">
<div class="wait-container" v-if="isLoading">
<p class="wait-msg">{{ $t('chat-room-list-connecting') }}</p>
</div>
<div class="wait-container" v-else-if="!rooms || rooms.length === 0">
<p class="wait-msg">{{ $t('chat-room-list-empty') }}</p>
<button class="retry-btn" @click="refreshRooms()">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="scroll-area">
<div class="scroll-area" v-else>
<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 }" @click="emit('select-room')">
<div class="room-content-wrapper">
@@ -48,21 +52,27 @@ const route = useRoute();
const showCreate = ref(false);
const rooms = ref<Room[]>([]);
const unreadCounts = ref<Record<string, number>>({});
const isLoading = ref(true);
async function refreshRooms() {
isLoading.value = true;
try {
const fetchedRooms = await fetchRooms();
rooms.value = fetchedRooms;
fetchedRooms.forEach(room => {
console.log(`Unread count for room ${room.name}: ${room.unread_count}`);
// If the room isn't the currently active one, store the count
if (room.unread_count && route.params.uuid !== room.uuid) {
unreadCounts.value[room.uuid] = room.unread_count;
} else {
unreadCounts.value[room.uuid] = 0;
}
});
} catch (error) {
console.error("Failed to refresh rooms", error);
} finally {
isLoading.value = false;
}
}
const incrementUnread = (roomUuid: string) => {
@@ -72,7 +82,7 @@ const incrementUnread = (roomUuid: string) => {
unreadCounts.value[roomUuid] = current + 1;
};
defineExpose({ incrementUnread });
defineExpose({ incrementUnread, refreshRooms });
watch(
() => route.params.uuid,
@@ -85,7 +95,6 @@ watch(
);
onMounted(async () => {
// rooms.value = await fetchRooms();
await refreshRooms()
});
</script>
@@ -111,6 +120,7 @@ onMounted(async () => {
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
width: 100%;
}
.wait-msg {
@@ -162,7 +172,6 @@ onMounted(async () => {
}
.rooms-header h2 {
/* font-size: 1rem; */
margin: 0;
margin-left: 45px;
}
@@ -189,7 +198,6 @@ onMounted(async () => {
}
.room-item.active {
/* border: 1px solid var(--border); */
background: var(--panel-accent);
color: var(--accent);
}

View File

@@ -25,6 +25,7 @@ auth-error-email-invalid = Please enter a valid email address
## Chat page
chat-no-room = Select a room to start talking
chat-no-messages = No messages yet. Say hi!
chat-input-placeholder = type a message
chat-invite-title = Invite People
chat-invite-receiver = Receiver username
@@ -32,8 +33,16 @@ chat-invite-friend-too = Also send a friend request
chat-invite-send = Send
chat-invite-username-placeholder = username
chat-room-list-title = Rooms
chat-room-list-empty = No rooms found
chat-room-list-connecting = Connecting...
chat-room-owner = by {$owner}
chat-room-global = Global room
chat-room-members = Members
chat-room-actions-leave = Leave Room
chat-room-actions-leave-confirm = Are you sure you want to leave this room?
chat-room-actions-delete = Delete Room
chat-room-actions-delete-confirm = Are you sure you want to delete this room? This cannot be undone.
chat-room-actions-ownership = Transfer Ownership
chat-create-title = Create room
chat-create-name = Room name
chat-create-name-placeholder = room name
@@ -95,6 +104,9 @@ warning-wrongversion-dismiss = I know what I'm doing
## Shared
shared-cancel = Cancel
shared-close = Close
shared-error = An error occurred
shared-save = Save
shared-updating = Updating
shared-delete = Delete
shared-leave = Leave

View File

@@ -25,8 +25,10 @@ auth-error-email-invalid = Veuillez entrer une adresse email valide
## Chat page
chat-no-room = Sélectionnez un salon pour commencer à discuter
chat-no-messages = Pas encore de messages. Dites bonjour !
chat-input-placeholder = tapez un message
chat-invite-title = Inviter des gens
chat-room-list-empty = Aucun salon trouvé
chat-invite-receiver = Nom du destinataire
chat-invite-friend-too = Envoyer aussi une demande d'ami
chat-invite-send = Envoyer
@@ -34,6 +36,13 @@ chat-invite-username-placeholder = nom d'utilisateur
chat-room-list-title = Salons
chat-room-list-connecting = Connexion...
chat-room-owner = par {$owner}
chat-room-global = Salon global
chat-room-members = Membres
chat-room-actions-leave = Quitter le Salon
chat-room-actions-leave-confirm = Voulez-vous vraiment quitter ce salon?
chat-room-actions-delete = Supprimer le Salon
chat-room-actions-delete-confirm = Voulez-vous vraiment supprimer ce salon? C'est irréversible.
chat-room-actions-ownership = Transférer la Propriété
chat-create-title = Créer un salon
chat-create-name = Nom du salon
chat-create-name-placeholder = nom du salon
@@ -93,6 +102,9 @@ warning-wrongversion-dismiss = Je sais ce que je fais
## Shared
shared-cancel = Annuler
shared-close = Fermer
shared-error = Une erreur est survenue
shared-save = Enregistrer
shared-updating = Mise à jour...
shared-delete = Supprimer
shared-leave = Quitter

View File

@@ -28,9 +28,9 @@ async function init() {
init()
// export const API = 'http://127.0.0.1:8080'
export const API = 'http://192.168.1.183:8080'
export const API = 'http://127.0.0.1:8080'
// export const API = 'http://192.168.1.183:8080'
// export const API = 'https://alatreon.org/frangipane'
// export const API_WS = 'ws://127.0.0.1:8080/ws'
export const API_WS = 'ws://192.168.1.183:8080/ws'
export const API_WS = 'ws://127.0.0.1:8080/ws'
// export const API_WS = 'ws://192.168.1.183:8080/ws'
// export const API_WS = 'wss://alatreon.org/frangipane/ws'

View File

@@ -13,27 +13,39 @@
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
<main class="chat-window-container" :class="{ 'sidebar-is-open': isSidebarOpen }">
<ChatWindow :uuid="uuid" @notification="handleNotification" />
<ChatWindow :uuid="safeUuid" @notification="handleNotification" @room-action="handleRoomAction" />
</main>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import RoomList from "../components/RoomList.vue";
import ChatWindow from "../components/ChatWindow.vue";
defineProps<{ uuid: string }>();
const isSidebarOpen = ref(true);
const props = defineProps<{ uuid?: string }>();
const router = useRouter();
const isSidebarOpen = ref(true);
const roomListRef = ref<InstanceType<typeof RoomList> | null>(null);
const safeUuid = computed(() => props.uuid || 'none');
const handleRoomSelection = () => {
if (window.innerWidth <= 720) {
isSidebarOpen.value = false;
}
};
const handleRoomAction = async () => {
if (roomListRef.value) {
await roomListRef.value.refreshRooms();
}
router.push('/rooms/none');
};
const handleNotification = (roomUuid: string) => {
if (roomListRef.value) {
roomListRef.value.incrementUnread(roomUuid);

View File

@@ -64,10 +64,6 @@ const handleAvatarError = (event: Event) => {
img.src = defaultAvatar;
};
async function handleAvatarUpdated() {
await fetchUserData()
}
const showAvatarModal = ref(false)
const router = useRouter()

View File

@@ -5,6 +5,11 @@ export interface User {
avatar_url: string
}
export interface UserProfile {
uuid: string
username: string
}
export interface LoginResponse {
uuid: string
username: string

View File

@@ -7,7 +7,7 @@ const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
base: './',
base: '/',
plugins: [vue()],