Merge pull request #41 from StewKI/feature-create-whiteboard-on-front

Feature create whiteboard on front
This commit is contained in:
Veljko
2026-02-18 20:34:10 +01:00
committed by GitHub
15 changed files with 136 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
using AipsCore.Application.Abstract.Query;
namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard;
public record GetWhiteboardQuery(string WhiteboardId) : IQuery<Infrastructure.Persistence.Whiteboard.Whiteboard?>;

View File

@@ -0,0 +1,23 @@
using AipsCore.Application.Abstract.Query;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard;
public class GetWhiteboardQueryHandler
: IQueryHandler<GetWhiteboardQuery, Infrastructure.Persistence.Whiteboard.Whiteboard?>
{
private readonly AipsDbContext _context;
public GetWhiteboardQueryHandler(AipsDbContext context)
{
_context = context;
}
public async Task<Infrastructure.Persistence.Whiteboard.Whiteboard?> Handle(GetWhiteboardQuery query, CancellationToken cancellationToken = default)
{
return await _context.Whiteboards
.Where(w => w.Id.ToString() == query.WhiteboardId)
.FirstOrDefaultAsync(cancellationToken);
}
}

View File

@@ -64,6 +64,23 @@ public static class UserContextRegistrationExtension
IssuerSigningKey = new SymmetricSecurityKey( IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings.Key)) Encoding.UTF8.GetBytes(jwtSettings.Key))
}; };
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}); });
services.AddAuthorization(); services.AddAuthorization();

View File

@@ -4,7 +4,7 @@
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"applicationUrl": "http://localhost:5039", "applicationUrl": "http://localhost:5039",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@@ -1,6 +1,7 @@
using AipsCore.Application.Abstract; using AipsCore.Application.Abstract;
using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard; using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard;
using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards; using AipsCore.Application.Models.Whiteboard.Query.GetRecentWhiteboards;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboard;
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardHistory; using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardHistory;
using AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership; using AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -22,12 +23,24 @@ public class WhiteboardController : ControllerBase
[Authorize] [Authorize]
[HttpPost] [HttpPost]
public async Task<ActionResult<int>> CreateWhiteboard(CreateWhiteboardCommand command, CancellationToken cancellationToken) public async Task<ActionResult<string>> CreateWhiteboard(CreateWhiteboardCommand command, CancellationToken cancellationToken)
{ {
var whiteboardId = await _dispatcher.Execute(command, cancellationToken); var whiteboardId = await _dispatcher.Execute(command, cancellationToken);
return Ok(whiteboardId.IdValue); return Ok(whiteboardId.IdValue);
} }
[Authorize]
[HttpGet("{whiteboardId}")]
public async Task<ActionResult<Whiteboard>> GetWhiteboardById([FromRoute] string whiteboardId, CancellationToken cancellationToken)
{
var whiteboard = await _dispatcher.Execute(new GetWhiteboardQuery(whiteboardId), cancellationToken);
if (whiteboard == null)
{
return NotFound();
}
return Ok(whiteboard);
}
[Authorize] [Authorize]
[HttpGet("history")] [HttpGet("history")]
public async Task<ActionResult<ICollection<Whiteboard>>> GetWhiteboardHistory(CancellationToken cancellationToken) public async Task<ActionResult<ICollection<Whiteboard>>> GetWhiteboardHistory(CancellationToken cancellationToken)

View File

@@ -2,11 +2,11 @@
import { onMounted, computed } from 'vue' import { onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useWhiteboardStore } from '@/stores/whiteboards' import { useWhiteboardsStore } from '@/stores/whiteboards'
import RecentWhiteboardsItem from './RecentWhiteboardsItem.vue' import RecentWhiteboardsItem from './RecentWhiteboardsItem.vue'
const router = useRouter() const router = useRouter()
const store = useWhiteboardStore() const store = useWhiteboardsStore()
onMounted(() => { onMounted(() => {
if (store.recentWhiteboards.length === 0) store.getRecentWhiteboards() if (store.recentWhiteboards.length === 0) store.getRecentWhiteboards()

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, computed } from 'vue' import { onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useWhiteboardStore } from '@/stores/whiteboards' import { useWhiteboardsStore } from '@/stores/whiteboards'
import WhiteboardHistoryItem from './WhiteboardHistoryItem.vue' import WhiteboardHistoryItem from './WhiteboardHistoryItem.vue'
const router = useRouter() const router = useRouter()
const store = useWhiteboardStore() const store = useWhiteboardsStore()
onMounted(() => { onMounted(() => {
if (store.ownedWhiteboards.length === 0) store.getWhiteboardHistory() if (store.ownedWhiteboards.length === 0) store.getWhiteboardHistory()

View File

@@ -52,7 +52,10 @@ router.beforeEach((to) => {
} }
if (to.meta.requiresAuth && !auth.isAuthenticated) { if (to.meta.requiresAuth && !auth.isAuthenticated) {
return '/login' return {
name: 'login',
query: { redirect: to.fullPath }
}
} }
}) })

View File

@@ -1,6 +1,3 @@
// A small fetch-based HTTP client with automatic token attach and refresh-on-401.
// This avoids circular imports by reading/writing tokens directly from localStorage.
const ACCESS_TOKEN = 'auth_token' const ACCESS_TOKEN = 'auth_token'
const REFRESH_TOKEN = 'refresh_token' const REFRESH_TOKEN = 'refresh_token'
@@ -9,7 +6,6 @@ async function parseJsonOrThrow(res: Response) {
try { try {
return text ? JSON.parse(text) : undefined return text ? JSON.parse(text) : undefined
} catch (e) { } catch (e) {
// non-JSON response
return text return text
} }
} }

View File

@@ -10,6 +10,14 @@ export const whiteboardService = {
async getRecentWhiteboards(): Promise<Whiteboard[]> { async getRecentWhiteboards(): Promise<Whiteboard[]> {
const raw = await api.get<any[]>('/api/Whiteboard/recent') const raw = await api.get<any[]>('/api/Whiteboard/recent')
return raw.map(mapWhiteboard) return raw.map(mapWhiteboard)
},
async createNewWhiteboard(title: string): Promise<string> {
return await api.post<string>('/api/Whiteboard', { title: title, maxParticipants: 10, joinPolicy: 0})
},
async getWhiteboardById(id: string): Promise<Whiteboard> {
return await api.get<any>(`/api/Whiteboard/${id}`).then(mapWhiteboard)
} }
} }

View File

@@ -1,7 +1,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { User, LoginCredentials, SignupCredentials, AuthResponse } from '@/types' import type { User, LoginCredentials, SignupCredentials, AuthResponse } from '@/types'
import { useWhiteboardStore } from '@/stores/whiteboards' import { useWhiteboardsStore } from '@/stores/whiteboards'
import { authService } from '@/services/authService' import { authService } from '@/services/authService'
const ACCESS_TOKEN = 'auth_token' const ACCESS_TOKEN = 'auth_token'
@@ -129,7 +129,7 @@ export const useAuthStore = defineStore('auth', () => {
} }
async function logout(allDevices = false) { async function logout(allDevices = false) {
const whiteboardStore = useWhiteboardStore() const whiteboardStore = useWhiteboardsStore()
isLoading.value = true isLoading.value = true
error.value = null error.value = null
try { try {

View File

@@ -3,7 +3,7 @@ import { ref } from 'vue'
import type { Whiteboard } from '@/types' import type { Whiteboard } from '@/types'
import { whiteboardService } from '@/services/whiteboardService' import { whiteboardService } from '@/services/whiteboardService'
export const useWhiteboardStore = defineStore('whiteboards', () => { export const useWhiteboardsStore = defineStore('whiteboards', () => {
const ownedWhiteboards = ref<Whiteboard[]>([]) const ownedWhiteboards = ref<Whiteboard[]>([])
const recentWhiteboards = ref<Whiteboard[]>([]) const recentWhiteboards = ref<Whiteboard[]>([])
const isLoading = ref(false) const isLoading = ref(false)
@@ -33,6 +33,25 @@ export const useWhiteboardStore = defineStore('whiteboards', () => {
} }
} }
async function createNewWhiteboard(title: string): Promise<string> {
let newWhiteboard: Whiteboard;
isLoading.value = true
error.value = null
try {
const newId = await whiteboardService.createNewWhiteboard(title)
newWhiteboard = await whiteboardService.getWhiteboardById(newId)
} catch (err: any) {
error.value = err.message ?? 'Failed to create whiteboard'
throw err
} finally {
isLoading.value = false
}
ownedWhiteboards.value.push(newWhiteboard)
return newWhiteboard.id;
}
function clearWhiteboards() { function clearWhiteboards() {
ownedWhiteboards.value = [] ownedWhiteboards.value = []
recentWhiteboards.value = [] recentWhiteboards.value = []
@@ -45,6 +64,7 @@ export const useWhiteboardStore = defineStore('whiteboards', () => {
error, error,
getWhiteboardHistory: getWhiteboardHistory, getWhiteboardHistory: getWhiteboardHistory,
getRecentWhiteboards: getRecentWhiteboards, getRecentWhiteboards: getRecentWhiteboards,
createNewWhiteboard: createNewWhiteboard,
clearWhiteboards: clearWhiteboards clearWhiteboards: clearWhiteboards
} }
}) })

View File

@@ -1,13 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router'
import WhiteboardHistorySidebar from '@/components/WhiteboardHistorySidebar.vue' 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";
const auth = useAuthStore() const auth = useAuthStore()
const whiteboards = useWhiteboardsStore()
const router = useRouter()
const joinCode = ref('') const joinCode = ref('')
const whiteboardTitle = ref('') const whiteboardTitle = ref('')
const showCreateModal = ref(false) const showCreateModal = ref(false)
async function handleCreateNewWhiteboard() {
if (!whiteboardTitle.value.trim()) {
alert('Please enter a title for the whiteboard.')
return
}
try {
const newWhiteboardId = await whiteboards.createNewWhiteboard(whiteboardTitle.value.trim())
showCreateModal.value = false
whiteboardTitle.value = ''
await router.push({ name: 'whiteboard', params: { id: newWhiteboardId } })
} catch (e) {
console.error('Failed to create new whiteboard', e)
}
}
</script> </script>
<template> <template>
@@ -80,7 +105,10 @@ const showCreateModal = ref(false)
</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>
<button type="button" class="btn btn-primary">Create</button> <button type="button" class="btn btn-primary" @click="handleCreateNewWhiteboard">
<span v-if="whiteboards.isLoading" class="spinner-border spinner-border-sm me-2"></span>
Create
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -26,10 +26,13 @@ async function handleLeave() {
</script> </script>
<template> <template>
<div v-if="store.isLoading" class="d-flex justify-content-center align-items-center vh-100"> <div v-if="store.isLoading" class="d-flex flex-column justify-content-center align-items-center vh-100">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<p class="text-muted fs-5 text-center">
Please wait while your whiteboard is loading...
</p>
</div> </div>
<div v-else-if="store.error" class="d-flex justify-content-center align-items-center vh-100"> <div v-else-if="store.error" class="d-flex justify-content-center align-items-center vh-100">

View File

@@ -22,7 +22,7 @@ export default defineConfig({
target: 'http://localhost:5266', target: 'http://localhost:5266',
changeOrigin: true changeOrigin: true
}, },
'/hubs/': { '/hubs': {
target: 'http://localhost:5039', target: 'http://localhost:5039',
changeOrigin: true, changeOrigin: true,
ws: true ws: true