Merge pull request #41 from StewKI/feature-create-whiteboard-on-front
Feature create whiteboard on front
This commit is contained in:
@@ -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?>;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,11 +23,23 @@ 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")]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Whiteboard } from "@/types";
|
import type {Whiteboard} from "@/types";
|
||||||
import { api } from './api'
|
import {api} from './api'
|
||||||
|
|
||||||
export const whiteboardService = {
|
export const whiteboardService = {
|
||||||
async getWhiteboardHistory(): Promise<Whiteboard[]> {
|
async getWhiteboardHistory(): Promise<Whiteboard[]> {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user