ui polish
This commit is contained in:
@@ -5,6 +5,7 @@ using AipsCore.Application.Models.Whiteboard.Command.RejectUserRequestToJoin;
|
|||||||
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
|
using AipsCore.Application.Models.Whiteboard.Query.GetMembershipStatus;
|
||||||
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
|
using AipsCore.Domain.Models.WhiteboardMembership.Enums;
|
||||||
using AipsRT.Model.Memberships;
|
using AipsRT.Model.Memberships;
|
||||||
|
using AipsRT.Model.Users;
|
||||||
using AipsRT.Model.Whiteboard;
|
using AipsRT.Model.Whiteboard;
|
||||||
using AipsRT.Model.Whiteboard.Shapes;
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
using AipsRT.Model.Whiteboard.Structs;
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
@@ -28,6 +29,23 @@ public class WhiteboardHub : Hub
|
|||||||
_membershipService = membershipService;
|
_membershipService = membershipService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
var userId = CurrentUserId;
|
||||||
|
var whiteboard = _whiteboardManager.GetWhiteboardForUser(userId);
|
||||||
|
|
||||||
|
if (whiteboard != null)
|
||||||
|
{
|
||||||
|
whiteboard.RemoveActiveUser(userId);
|
||||||
|
_whiteboardManager.RemoveUserFromWhiteboard(userId);
|
||||||
|
|
||||||
|
await Clients.Group(whiteboard.WhiteboardId.ToString())
|
||||||
|
.SendAsync("Leaved", userId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task JoinWhiteboard(Guid whiteboardId)
|
public async Task JoinWhiteboard(Guid whiteboardId)
|
||||||
{
|
{
|
||||||
if (!_whiteboardManager.WhiteboardExists(whiteboardId))
|
if (!_whiteboardManager.WhiteboardExists(whiteboardId))
|
||||||
@@ -56,11 +74,28 @@ public class WhiteboardHub : Hub
|
|||||||
if (status == WhiteboardMembershipStatus.Accepted)
|
if (status == WhiteboardMembershipStatus.Accepted)
|
||||||
{
|
{
|
||||||
_whiteboardManager.AddUserToWhiteboard(userId, whiteboardId);
|
_whiteboardManager.AddUserToWhiteboard(userId, whiteboardId);
|
||||||
|
|
||||||
|
var joiningUser = whiteboard.Users.FirstOrDefault(u => u.UserId == userId);
|
||||||
|
if (joiningUser == null)
|
||||||
|
{
|
||||||
|
if (ownerId == userId)
|
||||||
|
{
|
||||||
|
joiningUser = whiteboard.Owner;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
joiningUser = new User(userId, Context.User?.Identity?.Name ?? "Unknown",
|
||||||
|
"");
|
||||||
|
whiteboard.AddUser(joiningUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whiteboard.AddActiveUser(joiningUser);
|
||||||
|
|
||||||
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
|
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
|
||||||
await Clients.Caller.SendAsync("InitWhiteboard", state);
|
await Clients.Caller.SendAsync("InitWhiteboard", state);
|
||||||
|
|
||||||
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId).SendAsync("Joined", Context.UserIdentifier!);
|
await Clients.GroupExcept(whiteboardId.ToString(),
|
||||||
|
Context.ConnectionId).SendAsync("Joined", joiningUser);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -104,8 +139,13 @@ public class WhiteboardHub : Hub
|
|||||||
|
|
||||||
public async Task LeaveWhiteboard(Guid whiteboardId)
|
public async Task LeaveWhiteboard(Guid whiteboardId)
|
||||||
{
|
{
|
||||||
|
var userId = CurrentUserId;
|
||||||
|
_whiteboardManager.RemoveUserFromWhiteboard(userId);
|
||||||
|
_whiteboardManager.GetWhiteboard(whiteboardId)?.RemoveActiveUser(userId);
|
||||||
|
|
||||||
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
|
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
|
||||||
.SendAsync("Leaved", Context.UserIdentifier!);
|
.SendAsync("Leaved", Context.UserIdentifier!);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid CurrentUserId => Guid.Parse(Context.UserIdentifier!);
|
private Guid CurrentUserId => Guid.Parse(Context.UserIdentifier!);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class GetWhiteboardService
|
|||||||
{
|
{
|
||||||
WhiteboardId = entity.Id,
|
WhiteboardId = entity.Id,
|
||||||
OwnerId = entity.OwnerId,
|
OwnerId = entity.OwnerId,
|
||||||
|
Owner = new User(entity.Owner.Id, entity.Owner.UserName!, entity.Owner.Email!)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var membership in entity.Memberships)
|
foreach (var membership in entity.Memberships)
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ public class Whiteboard
|
|||||||
public Guid WhiteboardId { get; set; }
|
public Guid WhiteboardId { get; set; }
|
||||||
|
|
||||||
public Guid OwnerId { get; set; }
|
public Guid OwnerId { get; set; }
|
||||||
|
public User Owner { get; set; } = null!;
|
||||||
|
|
||||||
public List<User> Users { get; } = [];
|
public List<User> Users { get; } = [];
|
||||||
|
|
||||||
|
public List<User> ActiveUsers { get; } = [];
|
||||||
|
public void AddActiveUser(User user) => ActiveUsers.Add(user);
|
||||||
|
public void RemoveActiveUser(Guid userId)
|
||||||
|
=> ActiveUsers.RemoveAll(u => u.UserId == userId);
|
||||||
|
|
||||||
public List<Shape> Shapes { get; } = [];
|
public List<Shape> Shapes { get; } = [];
|
||||||
|
|
||||||
public List<Rectangle> Rectangles { get; } = [];
|
public List<Rectangle> Rectangles { get; } = [];
|
||||||
|
|||||||
@@ -4,3 +4,7 @@
|
|||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
margin: 4rem auto;
|
margin: 4rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,5 +22,3 @@ $border-color: #2a2a2a;
|
|||||||
|
|
||||||
$link-color: $primary;
|
$link-color: $primary;
|
||||||
|
|
||||||
$btn-close-color: #e5e7eb;
|
|
||||||
$btn-close-filter: invert(1);
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const showLogoutConfirm = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -27,9 +29,6 @@ const auth = useAuthStore()
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<RouterLink class="nav-link" active-class="active" to="/test">Test</RouterLink>
|
<RouterLink class="nav-link" active-class="active" to="/test">Test</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<RouterLink class="nav-link" active-class="active" to="/about">About</RouterLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
@@ -38,7 +37,7 @@ const auth = useAuthStore()
|
|||||||
<span class="nav-link text-light">{{ auth.user?.username }}</span>
|
<span class="nav-link text-light">{{ auth.user?.username }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="btn btn-outline-light btn-sm my-1" @click="auth.logout()">
|
<button class="btn btn-outline-light btn-sm my-1" @click="showLogoutConfirm = true">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -55,4 +54,46 @@ const auth = useAuthStore()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="showLogoutConfirm"
|
||||||
|
class="modal d-block"
|
||||||
|
tabindex="-1"
|
||||||
|
style="background: rgba(0,0,0,0.5)"
|
||||||
|
@click.self="showLogoutConfirm = false"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header border-0 pb-0">
|
||||||
|
<h5 class="modal-title">Logout</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
@click="showLogoutConfirm = false"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you sure you want to logout?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 pt-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
@click="showLogoutConfirm = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-danger"
|
||||||
|
@click="showLogoutConfirm = false; auth.logout()"
|
||||||
|
>
|
||||||
|
Yes, logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const handleClick = () => emit('click', props.whiteboard)
|
|||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<h5 class="mb-0 text-dark">{{ whiteboard.title }}</h5>
|
<h5 class="mb-0">{{ whiteboard.title }}</h5>
|
||||||
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import RecentWhiteboardsList from './RecentWhiteboardsList.vue'
|
|||||||
</h5>
|
</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-close btn-close-white"
|
class="btn-close"
|
||||||
data-bs-dismiss="offcanvas"
|
data-bs-dismiss="offcanvas"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const handleConfirmDelete = (e: MouseEvent) => {
|
|||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<h5 class="mb-0 text-dark">{{ whiteboard.title }}</h5>
|
<h5 class="mb-0">{{ whiteboard.title }}</h5>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import WhiteboardHistoryList from './WhiteboardHistoryList.vue'
|
|||||||
</h5>
|
</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-close btn-close-white"
|
class="btn-close"
|
||||||
data-bs-dismiss="offcanvas"
|
data-bs-dismiss="offcanvas"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,18 +2,21 @@
|
|||||||
import {computed, ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
|
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
|
||||||
import { useWhiteboardsStore } from "@/stores/whiteboards.ts";
|
import { useWhiteboardsStore } from "@/stores/whiteboards.ts";
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { avatarColorFromString } from '@/composables/useShapeOwner'
|
||||||
import type { ShapeTool, Arrow, Line, Rectangle, TextShape } from '@/types/whiteboard.ts'
|
import type { ShapeTool, Arrow, Line, Rectangle, TextShape } from '@/types/whiteboard.ts'
|
||||||
|
|
||||||
const sessionStore = useWhiteboardStore()
|
const sessionStore = useWhiteboardStore()
|
||||||
const infoStore = useWhiteboardsStore()
|
const infoStore = useWhiteboardsStore()
|
||||||
|
const auth = useAuthStore()
|
||||||
const emit = defineEmits<{ leave: [] }>()
|
const emit = defineEmits<{ leave: [] }>()
|
||||||
|
|
||||||
const tools: { name: ShapeTool; label: string; icon: string; enabled: boolean }[] = [
|
const tools: { name: ShapeTool; label: string; icon: string; enabled: boolean }[] = [
|
||||||
{ name: 'hand', label: 'Select', icon: '\u270B', enabled: true },
|
{ name: 'hand', label: 'Select', icon: 'bi-cursor', enabled: true },
|
||||||
{ name: 'rectangle', label: 'Rectangle', icon: '\u25AD', enabled: true },
|
{ name: 'rectangle', label: 'Rectangle', icon: 'bi-square', enabled: true },
|
||||||
{ name: 'arrow', label: 'Arrow', icon: '\u2192', enabled: true },
|
{ name: 'arrow', label: 'Arrow', icon: 'bi-arrow-up-right', enabled: true },
|
||||||
{ name: 'line', label: 'Line', icon: '\u2571', enabled: true },
|
{ name: 'line', label: 'Line', icon: 'bi-slash-lg', enabled: true },
|
||||||
{ name: 'text', label: 'Text', icon: 'T', enabled: true },
|
{ name: 'text', label: 'Text', icon: 'bi-fonts', enabled: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
const colors = ['#4f9dff', '#ff4f4f', '#4fff4f', '#ffff4f', '#ff4fff', '#ffffff', '#ff9f4f', '#4fffff']
|
const colors = ['#4f9dff', '#ff4f4f', '#4fff4f', '#ffff4f', '#ff4fff', '#ffffff', '#ff9f4f', '#4fffff']
|
||||||
@@ -86,7 +89,7 @@ const copyCodeToClipboard = async () => {
|
|||||||
:title="tool.enabled ? tool.label : `${tool.label} (coming soon)`"
|
:title="tool.enabled ? tool.label : `${tool.label} (coming soon)`"
|
||||||
@click="tool.enabled && sessionStore.selectTool(tool.name)"
|
@click="tool.enabled && sessionStore.selectTool(tool.name)"
|
||||||
>
|
>
|
||||||
{{ tool.icon }}
|
<i :class="['bi', tool.icon]"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -140,6 +143,28 @@ const copyCodeToClipboard = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sessionStore.connectedUsers.length" class="participants-panel">
|
||||||
|
<div class="property-label">Participants ({{ sessionStore.connectedUsers.length }})</div>
|
||||||
|
<div class="participants-list">
|
||||||
|
<div
|
||||||
|
v-for="user in sessionStore.connectedUsers"
|
||||||
|
:key="user.userId"
|
||||||
|
class="participant"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="participant-avatar"
|
||||||
|
:style="{ backgroundColor: avatarColorFromString(user.username || user.userId) }"
|
||||||
|
>
|
||||||
|
{{ (user.username || '?').charAt(0).toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
<span class="participant-name">
|
||||||
|
{{ user.username || 'Unknown' }}
|
||||||
|
<span v-if="user.userId === auth.user?.userId" class="you-label">(You)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-footer">
|
<div class="toolbar-footer">
|
||||||
<div class="position-relative mb-2">
|
<div class="position-relative mb-2">
|
||||||
<button
|
<button
|
||||||
@@ -253,6 +278,52 @@ const copyCodeToClipboard = async () => {
|
|||||||
accent-color: #4f9dff;
|
accent-color: #4f9dff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.participants-panel {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-top: 1px solid #2a2a3e;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-avatar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.you-label {
|
||||||
|
color: #8a8a9e;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-footer {
|
.toolbar-footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ref, watch, type Ref } from 'vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { fetchUser } from '@/services/userService'
|
import { fetchUser } from '@/services/userService'
|
||||||
|
|
||||||
function avatarColorFromString(str: string): string {
|
export function avatarColorFromString(str: string): string {
|
||||||
let hash = 5381
|
let hash = 5381
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff
|
hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ const router = createRouter({
|
|||||||
component: () => import('../views/TestView.vue'),
|
component: () => import('../views/TestView.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requiresAuth: false },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'about',
|
|
||||||
component: () => import('../views/AboutView.vue'),
|
|
||||||
meta: { requiresAuth: false },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export const whiteboardHubService = {
|
|||||||
client.on<MoveShapeCommand>('MovedShape', callback)
|
client.on<MoveShapeCommand>('MovedShape', callback)
|
||||||
},
|
},
|
||||||
|
|
||||||
onJoined(callback: (userId: string) => void) {
|
onJoined(callback: (user: User) => void) {
|
||||||
client.on<string>('Joined', callback)
|
client.on<User>('Joined', callback)
|
||||||
},
|
},
|
||||||
|
|
||||||
onLeaved(callback: (userId: string) => void) {
|
onLeaved(callback: (userId: string) => void) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {User} from "@/types";
|
|||||||
export const useWhiteboardStore = defineStore('whiteboard', () => {
|
export const useWhiteboardStore = defineStore('whiteboard', () => {
|
||||||
const whiteboard = ref<Whiteboard | null>(null)
|
const whiteboard = ref<Whiteboard | null>(null)
|
||||||
const pendingUsers = ref<User[]>([])
|
const pendingUsers = ref<User[]>([])
|
||||||
|
const connectedUsers = ref<User[]>([])
|
||||||
|
|
||||||
const selectedTool = ref<ShapeTool>('hand')
|
const selectedTool = ref<ShapeTool>('hand')
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
@@ -51,8 +52,10 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
|
|||||||
|
|
||||||
function registerHubEvents() {
|
function registerHubEvents() {
|
||||||
whiteboardHubService.onInitWhiteboard((wb) => {
|
whiteboardHubService.onInitWhiteboard((wb) => {
|
||||||
|
console.log('InitWhiteboard payload:', JSON.stringify(wb, null, 2))
|
||||||
deselectShape()
|
deselectShape()
|
||||||
whiteboard.value = wb
|
whiteboard.value = wb
|
||||||
|
connectedUsers.value = wb.activeUsers ?? []
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,12 +79,14 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
|
|||||||
applyMoveShape(command.shapeId, command.newPositionX, command.newPositionY)
|
applyMoveShape(command.shapeId, command.newPositionX, command.newPositionY)
|
||||||
})
|
})
|
||||||
|
|
||||||
whiteboardHubService.onJoined((userId) => {
|
whiteboardHubService.onJoined((user) => {
|
||||||
console.log('User joined:', userId)
|
if (!connectedUsers.value.some(u => u.userId === user.userId)) {
|
||||||
|
connectedUsers.value.push(user)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
whiteboardHubService.onLeaved((userId) => {
|
whiteboardHubService.onLeaved((userId) => {
|
||||||
console.log('User left:', userId)
|
connectedUsers.value = connectedUsers.value.filter(u => u.userId !== userId)
|
||||||
})
|
})
|
||||||
|
|
||||||
whiteboardHubService.onWaitingForApproval(() => {
|
whiteboardHubService.onWaitingForApproval(() => {
|
||||||
@@ -155,6 +160,7 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
|
|||||||
await whiteboardHubService.disconnect()
|
await whiteboardHubService.disconnect()
|
||||||
|
|
||||||
whiteboard.value = null
|
whiteboard.value = null
|
||||||
|
connectedUsers.value = []
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
selectedTool.value = 'hand'
|
selectedTool.value = 'hand'
|
||||||
deselectShape()
|
deselectShape()
|
||||||
@@ -251,6 +257,7 @@ export const useWhiteboardStore = defineStore('whiteboard', () => {
|
|||||||
toolThickness,
|
toolThickness,
|
||||||
toolTextSize,
|
toolTextSize,
|
||||||
pendingUsers,
|
pendingUsers,
|
||||||
|
connectedUsers,
|
||||||
approveUser,
|
approveUser,
|
||||||
rejectUser,
|
rejectUser,
|
||||||
cancelJoinRequest,
|
cancelJoinRequest,
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface Whiteboard {
|
|||||||
arrows: Arrow[]
|
arrows: Arrow[]
|
||||||
lines: Line[]
|
lines: Line[]
|
||||||
textShapes: TextShape[]
|
textShapes: TextShape[]
|
||||||
|
users: import('@/types').User[]
|
||||||
|
activeUsers: import('@/types').User[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoveShapeCommand {
|
export interface MoveShapeCommand {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h1>About</h1>
|
|
||||||
</template>
|
|
||||||
@@ -6,7 +6,7 @@ import WhiteboardHistorySidebar from '@/components/WhiteboardHistorySidebar.vue'
|
|||||||
import RecentWhiteboardsPanel from '@/components/RecentWhiteboardsPanel.vue'
|
import RecentWhiteboardsPanel from '@/components/RecentWhiteboardsPanel.vue'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {useWhiteboardsStore} from "@/stores/whiteboards.ts";
|
import {useWhiteboardsStore} from "@/stores/whiteboards.ts";
|
||||||
import {MembershipStatus} from "@/enums";
|
import {MembershipStatus, WhiteboardJoinPolicy} from "@/enums";
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const whiteboards = useWhiteboardsStore()
|
const whiteboards = useWhiteboardsStore()
|
||||||
@@ -14,6 +14,8 @@ const router = useRouter()
|
|||||||
|
|
||||||
const joinCode = ref('')
|
const joinCode = ref('')
|
||||||
const whiteboardTitle = ref('')
|
const whiteboardTitle = ref('')
|
||||||
|
const selectedJoinPolicy = ref(WhiteboardJoinPolicy.FreeToJoin)
|
||||||
|
const maxParticipants = ref(10)
|
||||||
const showCreateModal = ref(false)
|
const showCreateModal = ref(false)
|
||||||
|
|
||||||
async function handleCreateNewWhiteboard() {
|
async function handleCreateNewWhiteboard() {
|
||||||
@@ -27,6 +29,8 @@ async function handleCreateNewWhiteboard() {
|
|||||||
|
|
||||||
showCreateModal.value = false
|
showCreateModal.value = false
|
||||||
whiteboardTitle.value = ''
|
whiteboardTitle.value = ''
|
||||||
|
selectedJoinPolicy.value = WhiteboardJoinPolicy.FreeToJoin
|
||||||
|
maxParticipants.value = 10
|
||||||
|
|
||||||
await router.push({ name: 'whiteboard', params: { id: newWhiteboardId } })
|
await router.push({ name: 'whiteboard', params: { id: newWhiteboardId } })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -124,6 +128,23 @@ async function joinWithCode() {
|
|||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
placeholder="Whiteboard title"
|
placeholder="Whiteboard title"
|
||||||
/>
|
/>
|
||||||
|
<label class="form-label mt-3 mb-1">Join Policy</label>
|
||||||
|
<select
|
||||||
|
v-model="selectedJoinPolicy"
|
||||||
|
class="form-select bg-dark text-light border-secondary"
|
||||||
|
>
|
||||||
|
<option :value="WhiteboardJoinPolicy.FreeToJoin">Free to Join</option>
|
||||||
|
<option :value="WhiteboardJoinPolicy.RequestToJoin">Request to Join</option>
|
||||||
|
<option :value="WhiteboardJoinPolicy.Private">Private</option>
|
||||||
|
</select>
|
||||||
|
<label class="form-label mt-3 mb-1">Max Participants</label>
|
||||||
|
<input
|
||||||
|
v-model.number="maxParticipants"
|
||||||
|
type="number"
|
||||||
|
min="2"
|
||||||
|
max="50"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-secondary">
|
<div class="modal-footer border-secondary">
|
||||||
<button type="button" class="btn btn-secondary" @click="showCreateModal = false">Cancel</button>
|
<button type="button" class="btn btn-secondary" @click="showCreateModal = false">Cancel</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user