added room actions: leave and delete, and improved connection error handling
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "frangipane-client",
|
"name": "frangipane-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"backendVersion": "1.0.2",
|
"backendVersion": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "frangipane",
|
"productName": "frangipane",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"identifier": "com.strawberries.frangipane",
|
"identifier": "com.strawberries.frangipane",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "yarn dev",
|
"beforeDevCommand": "yarn dev",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { apiFetch } from './client'
|
import { apiFetch } from './client'
|
||||||
import type { Room, RoomInvite } from '../types'
|
import { UserProfile, type Room, type RoomInvite } from '../types'
|
||||||
|
|
||||||
export function fetchRooms() {
|
export function fetchRooms() {
|
||||||
return apiFetch<Room[]>(`/rooms`)
|
return apiFetch<Room[]>(`/rooms`)
|
||||||
@@ -40,3 +40,26 @@ export function declineRoomInvite(senderUuid: string, roomUuid: string) {
|
|||||||
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
|
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`)
|
||||||
|
}
|
||||||
|
|||||||
10
src/base.css
10
src/base.css
@@ -24,6 +24,7 @@ body,
|
|||||||
--muted: #9aa0aa;
|
--muted: #9aa0aa;
|
||||||
--accent: #f27aa3;
|
--accent: #f27aa3;
|
||||||
--accent-hover: #ff91b3;
|
--accent-hover: #ff91b3;
|
||||||
|
--accent-second: #96CDFB;
|
||||||
--border: #2a2f3b;
|
--border: #2a2f3b;
|
||||||
--error: #ff5050;
|
--error: #ff5050;
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
@@ -174,6 +175,15 @@ i:hover {
|
|||||||
margin: 30px;
|
margin: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
i:hover {
|
i:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<InvitePeopleModal v-if="showInviteModal && isSocketConnected" :room_uuid="props.uuid"
|
<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 v-if="uuid === 'none'" class="no-room">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -10,15 +14,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="chat-container">
|
<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">
|
<div class="messages-container" ref="messageListRef" @scroll="handleScroll">
|
||||||
<MessageList v-if="messages.length > 0 || isSocketConnected" :messages="messages" />
|
<MessageList v-if="messages.length > 0 || isSocketConnected" :messages="messages" />
|
||||||
</div>
|
</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"
|
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
|
||||||
:title="$t('chat-invite-title')">
|
:title="$t('chat-invite-title')">
|
||||||
<i class="fa-solid fa-users"></i>
|
<i class="fa-solid fa-users"></i>
|
||||||
@@ -26,9 +45,6 @@
|
|||||||
|
|
||||||
<div v-if="connectionError" class="connection-error">
|
<div v-if="connectionError" class="connection-error">
|
||||||
<p>{{ connectionError }}</p>
|
<p>{{ connectionError }}</p>
|
||||||
<button class="retry-btn" @click="retryConnection">
|
|
||||||
<i class="fa-solid fa-rotate-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MessageInput v-if="isSocketConnected" ref="messageInputRef" @send="onSend" />
|
<MessageInput v-if="isSocketConnected" ref="messageInputRef" @send="onSend" />
|
||||||
@@ -45,6 +61,7 @@ import { apiFetch } from '../api/client';
|
|||||||
import MessageList from "./MessageList.vue";
|
import MessageList from "./MessageList.vue";
|
||||||
import MessageInput from "./MessageInput.vue";
|
import MessageInput from "./MessageInput.vue";
|
||||||
import InvitePeopleModal from './InvitePeopleModal.vue';
|
import InvitePeopleModal from './InvitePeopleModal.vue';
|
||||||
|
import RoomDetailsModal from "./RoomDetailsModal.vue";
|
||||||
import WebSocket from '@tauri-apps/plugin-websocket';
|
import WebSocket from '@tauri-apps/plugin-websocket';
|
||||||
import { getAuthData } from "../store.ts";
|
import { getAuthData } from "../store.ts";
|
||||||
import { fetchRoomInfo } from "../api/rooms.ts";
|
import { fetchRoomInfo } from "../api/rooms.ts";
|
||||||
@@ -53,10 +70,11 @@ import { useFluent } from 'fluent-vue';
|
|||||||
const { $t } = useFluent();
|
const { $t } = useFluent();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
// (e: 'send', content: string): void
|
|
||||||
(e: 'notification', roomUuid: string): void
|
(e: 'notification', roomUuid: string): void
|
||||||
|
(e: 'room-action'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps<{ uuid: string }>();
|
const props = defineProps<{ uuid: string }>();
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
@@ -71,6 +89,8 @@ const connectionError = ref<string | null>(null);
|
|||||||
const isLoadingMore = ref(false);
|
const isLoadingMore = ref(false);
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
const showInviteModal = ref(false);
|
const showInviteModal = ref(false);
|
||||||
|
const showDetailsModal = ref(false);
|
||||||
|
const isInitialLoad = ref(false);
|
||||||
|
|
||||||
// WebSocket State
|
// WebSocket State
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
@@ -82,6 +102,11 @@ const isOwner = computed(() => {
|
|||||||
return currentUser.value.uuid === currentRoom.value.owner_uuid;
|
return currentUser.value.uuid === currentRoom.value.owner_uuid;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRoomChanged = () => {
|
||||||
|
showDetailsModal.value = false;
|
||||||
|
emit('room-action');
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await connectGlobalWebSocket();
|
await connectGlobalWebSocket();
|
||||||
|
|
||||||
@@ -109,12 +134,15 @@ async function retryConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadRoomData() {
|
async function loadRoomData() {
|
||||||
|
isInitialLoad.value = true;
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
currentRoom.value = null;
|
currentRoom.value = null;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
connectionError.value = null;
|
|
||||||
|
|
||||||
if (props.uuid === 'none') return;
|
if (props.uuid === 'none') {
|
||||||
|
isInitialLoad.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [msgs, roomInfo, auth] = await Promise.all([
|
const [msgs, roomInfo, auth] = await Promise.all([
|
||||||
@@ -132,6 +160,8 @@ async function loadRoomData() {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Room data load failed:", err);
|
console.error("Room data load failed:", err);
|
||||||
|
} finally {
|
||||||
|
isInitialLoad.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +207,7 @@ async function connectGlobalWebSocket() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("WS Connect failed:", err);
|
console.error("WS Connect failed:", err);
|
||||||
isSocketConnected.value = false;
|
isSocketConnected.value = false;
|
||||||
connectionError.value = "Live chat disconnected.";
|
connectionError.value = $t('shared-error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +344,14 @@ async function onSend(content: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-name {
|
.room-name {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -371,4 +409,44 @@ async function onSend(content: string) {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
opacity: 0.3;
|
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>
|
</style>
|
||||||
|
|||||||
97
src/components/ConfirmModal.vue
Normal file
97
src/components/ConfirmModal.vue
Normal 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>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
|
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
|
||||||
<div class="sender-info">
|
<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>
|
<div class="sender">{{ m.sender }}</div>
|
||||||
<span class="timestamp">{{ m.sent_at }}</span>
|
<span class="timestamp">{{ m.sent_at }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,15 +74,6 @@ ul {
|
|||||||
padding: 5px 18px;
|
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 {
|
.message.is-me {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|||||||
280
src/components/RoomDetailsModal.vue
Normal file
280
src/components/RoomDetailsModal.vue
Normal 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>
|
||||||
@@ -11,14 +11,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</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>
|
<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()">
|
<button class="retry-btn" @click="refreshRooms()">
|
||||||
<i class="fa-solid fa-rotate-right"></i>
|
<i class="fa-solid fa-rotate-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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"
|
<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')">
|
:class="{ active: route.params.uuid === room.uuid }" @click="emit('select-room')">
|
||||||
<div class="room-content-wrapper">
|
<div class="room-content-wrapper">
|
||||||
@@ -48,21 +52,27 @@ const route = useRoute();
|
|||||||
const showCreate = ref(false);
|
const showCreate = ref(false);
|
||||||
const rooms = ref<Room[]>([]);
|
const rooms = ref<Room[]>([]);
|
||||||
const unreadCounts = ref<Record<string, number>>({});
|
const unreadCounts = ref<Record<string, number>>({});
|
||||||
|
const isLoading = ref(true);
|
||||||
|
|
||||||
async function refreshRooms() {
|
async function refreshRooms() {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
const fetchedRooms = await fetchRooms();
|
const fetchedRooms = await fetchRooms();
|
||||||
rooms.value = fetchedRooms;
|
rooms.value = fetchedRooms;
|
||||||
|
|
||||||
fetchedRooms.forEach(room => {
|
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) {
|
if (room.unread_count && route.params.uuid !== room.uuid) {
|
||||||
unreadCounts.value[room.uuid] = room.unread_count;
|
unreadCounts.value[room.uuid] = room.unread_count;
|
||||||
} else {
|
} else {
|
||||||
unreadCounts.value[room.uuid] = 0;
|
unreadCounts.value[room.uuid] = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to refresh rooms", error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const incrementUnread = (roomUuid: string) => {
|
const incrementUnread = (roomUuid: string) => {
|
||||||
@@ -72,7 +82,7 @@ const incrementUnread = (roomUuid: string) => {
|
|||||||
unreadCounts.value[roomUuid] = current + 1;
|
unreadCounts.value[roomUuid] = current + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ incrementUnread });
|
defineExpose({ incrementUnread, refreshRooms });
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.uuid,
|
() => route.params.uuid,
|
||||||
@@ -85,7 +95,6 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// rooms.value = await fetchRooms();
|
|
||||||
await refreshRooms()
|
await refreshRooms()
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -111,6 +120,7 @@ onMounted(async () => {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wait-msg {
|
.wait-msg {
|
||||||
@@ -162,7 +172,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rooms-header h2 {
|
.rooms-header h2 {
|
||||||
/* font-size: 1rem; */
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: 45px;
|
margin-left: 45px;
|
||||||
}
|
}
|
||||||
@@ -189,7 +198,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-item.active {
|
.room-item.active {
|
||||||
/* border: 1px solid var(--border); */
|
|
||||||
background: var(--panel-accent);
|
background: var(--panel-accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ auth-error-email-invalid = Please enter a valid email address
|
|||||||
|
|
||||||
## Chat page
|
## Chat page
|
||||||
chat-no-room = Select a room to start talking
|
chat-no-room = Select a room to start talking
|
||||||
|
chat-no-messages = No messages yet. Say hi!
|
||||||
chat-input-placeholder = type a message
|
chat-input-placeholder = type a message
|
||||||
chat-invite-title = Invite People
|
chat-invite-title = Invite People
|
||||||
chat-invite-receiver = Receiver username
|
chat-invite-receiver = Receiver username
|
||||||
@@ -32,8 +33,16 @@ chat-invite-friend-too = Also send a friend request
|
|||||||
chat-invite-send = Send
|
chat-invite-send = Send
|
||||||
chat-invite-username-placeholder = username
|
chat-invite-username-placeholder = username
|
||||||
chat-room-list-title = Rooms
|
chat-room-list-title = Rooms
|
||||||
|
chat-room-list-empty = No rooms found
|
||||||
chat-room-list-connecting = Connecting...
|
chat-room-list-connecting = Connecting...
|
||||||
chat-room-owner = by {$owner}
|
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-title = Create room
|
||||||
chat-create-name = Room name
|
chat-create-name = Room name
|
||||||
chat-create-name-placeholder = room name
|
chat-create-name-placeholder = room name
|
||||||
@@ -95,6 +104,9 @@ warning-wrongversion-dismiss = I know what I'm doing
|
|||||||
|
|
||||||
## Shared
|
## Shared
|
||||||
shared-cancel = Cancel
|
shared-cancel = Cancel
|
||||||
|
shared-close = Close
|
||||||
shared-error = An error occurred
|
shared-error = An error occurred
|
||||||
shared-save = Save
|
shared-save = Save
|
||||||
shared-updating = Updating
|
shared-updating = Updating
|
||||||
|
shared-delete = Delete
|
||||||
|
shared-leave = Leave
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ auth-error-email-invalid = Veuillez entrer une adresse email valide
|
|||||||
|
|
||||||
## Chat page
|
## Chat page
|
||||||
chat-no-room = Sélectionnez un salon pour commencer à discuter
|
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-input-placeholder = tapez un message
|
||||||
chat-invite-title = Inviter des gens
|
chat-invite-title = Inviter des gens
|
||||||
|
chat-room-list-empty = Aucun salon trouvé
|
||||||
chat-invite-receiver = Nom du destinataire
|
chat-invite-receiver = Nom du destinataire
|
||||||
chat-invite-friend-too = Envoyer aussi une demande d'ami
|
chat-invite-friend-too = Envoyer aussi une demande d'ami
|
||||||
chat-invite-send = Envoyer
|
chat-invite-send = Envoyer
|
||||||
@@ -34,6 +36,13 @@ chat-invite-username-placeholder = nom d'utilisateur
|
|||||||
chat-room-list-title = Salons
|
chat-room-list-title = Salons
|
||||||
chat-room-list-connecting = Connexion...
|
chat-room-list-connecting = Connexion...
|
||||||
chat-room-owner = par {$owner}
|
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-title = Créer un salon
|
||||||
chat-create-name = Nom du salon
|
chat-create-name = Nom du salon
|
||||||
chat-create-name-placeholder = 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
|
||||||
shared-cancel = Annuler
|
shared-cancel = Annuler
|
||||||
|
shared-close = Fermer
|
||||||
shared-error = Une erreur est survenue
|
shared-error = Une erreur est survenue
|
||||||
shared-save = Enregistrer
|
shared-save = Enregistrer
|
||||||
shared-updating = Mise à jour...
|
shared-updating = Mise à jour...
|
||||||
|
shared-delete = Supprimer
|
||||||
|
shared-leave = Quitter
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ async function init() {
|
|||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
// export const API = 'http://127.0.0.1:8080'
|
export const API = 'http://127.0.0.1:8080'
|
||||||
export const API = 'http://192.168.1.183:8080'
|
// export const API = 'http://192.168.1.183:8080'
|
||||||
// export const API = 'https://alatreon.org/frangipane'
|
// export const API = 'https://alatreon.org/frangipane'
|
||||||
// export const API_WS = 'ws://127.0.0.1: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 = 'ws://192.168.1.183:8080/ws'
|
||||||
// export const API_WS = 'wss://alatreon.org/frangipane/ws'
|
// export const API_WS = 'wss://alatreon.org/frangipane/ws'
|
||||||
|
|||||||
@@ -13,27 +13,39 @@
|
|||||||
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
|
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
|
||||||
|
|
||||||
<main class="chat-window-container" :class="{ 'sidebar-is-open': isSidebarOpen }">
|
<main class="chat-window-container" :class="{ 'sidebar-is-open': isSidebarOpen }">
|
||||||
<ChatWindow :uuid="uuid" @notification="handleNotification" />
|
<ChatWindow :uuid="safeUuid" @notification="handleNotification" @room-action="handleRoomAction" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 RoomList from "../components/RoomList.vue";
|
||||||
import ChatWindow from "../components/ChatWindow.vue";
|
import ChatWindow from "../components/ChatWindow.vue";
|
||||||
|
|
||||||
defineProps<{ uuid: string }>();
|
const props = defineProps<{ uuid?: string }>();
|
||||||
const isSidebarOpen = ref(true);
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isSidebarOpen = ref(true);
|
||||||
const roomListRef = ref<InstanceType<typeof RoomList> | null>(null);
|
const roomListRef = ref<InstanceType<typeof RoomList> | null>(null);
|
||||||
|
|
||||||
|
const safeUuid = computed(() => props.uuid || 'none');
|
||||||
|
|
||||||
const handleRoomSelection = () => {
|
const handleRoomSelection = () => {
|
||||||
if (window.innerWidth <= 720) {
|
if (window.innerWidth <= 720) {
|
||||||
isSidebarOpen.value = false;
|
isSidebarOpen.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRoomAction = async () => {
|
||||||
|
if (roomListRef.value) {
|
||||||
|
await roomListRef.value.refreshRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push('/rooms/none');
|
||||||
|
};
|
||||||
|
|
||||||
const handleNotification = (roomUuid: string) => {
|
const handleNotification = (roomUuid: string) => {
|
||||||
if (roomListRef.value) {
|
if (roomListRef.value) {
|
||||||
roomListRef.value.incrementUnread(roomUuid);
|
roomListRef.value.incrementUnread(roomUuid);
|
||||||
|
|||||||
@@ -64,10 +64,6 @@ const handleAvatarError = (event: Event) => {
|
|||||||
img.src = defaultAvatar;
|
img.src = defaultAvatar;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleAvatarUpdated() {
|
|
||||||
await fetchUserData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAvatarModal = ref(false)
|
const showAvatarModal = ref(false)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export interface User {
|
|||||||
avatar_url: string
|
avatar_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
uuid: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
uuid: string
|
uuid: string
|
||||||
username: string
|
username: string
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const host = process.env.TAURI_DEV_HOST;
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
base: './',
|
base: '/',
|
||||||
|
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user