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(
|
||||
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();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5039",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AipsCore.Application.Abstract;
|
||||
using AipsCore.Application.Models.Whiteboard.Command.CreateWhiteboard;
|
||||
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.WhiteboardMembership.Command.CreateWhiteboardMembership;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -22,12 +23,24 @@ public class WhiteboardController : ControllerBase
|
||||
|
||||
[Authorize]
|
||||
[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);
|
||||
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]
|
||||
[HttpGet("history")]
|
||||
public async Task<ActionResult<ICollection<Whiteboard>>> GetWhiteboardHistory(CancellationToken cancellationToken)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useWhiteboardStore } from '@/stores/whiteboards'
|
||||
import { useWhiteboardsStore } from '@/stores/whiteboards'
|
||||
import RecentWhiteboardsItem from './RecentWhiteboardsItem.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useWhiteboardStore()
|
||||
const store = useWhiteboardsStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (store.recentWhiteboards.length === 0) store.getRecentWhiteboards()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useWhiteboardStore } from '@/stores/whiteboards'
|
||||
import { useWhiteboardsStore } from '@/stores/whiteboards'
|
||||
import WhiteboardHistoryItem from './WhiteboardHistoryItem.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useWhiteboardStore()
|
||||
const store = useWhiteboardsStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (store.ownedWhiteboards.length === 0) store.getWhiteboardHistory()
|
||||
|
||||
@@ -52,7 +52,10 @@ router.beforeEach((to) => {
|
||||
}
|
||||
|
||||
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 REFRESH_TOKEN = 'refresh_token'
|
||||
|
||||
@@ -9,7 +6,6 @@ async function parseJsonOrThrow(res: Response) {
|
||||
try {
|
||||
return text ? JSON.parse(text) : undefined
|
||||
} catch (e) {
|
||||
// non-JSON response
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Whiteboard } from "@/types";
|
||||
import { api } from './api'
|
||||
import type {Whiteboard} from "@/types";
|
||||
import {api} from './api'
|
||||
|
||||
export const whiteboardService = {
|
||||
async getWhiteboardHistory(): Promise<Whiteboard[]> {
|
||||
@@ -10,6 +10,14 @@ export const whiteboardService = {
|
||||
async getRecentWhiteboards(): Promise<Whiteboard[]> {
|
||||
const raw = await api.get<any[]>('/api/Whiteboard/recent')
|
||||
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 { defineStore } from 'pinia'
|
||||
import type { User, LoginCredentials, SignupCredentials, AuthResponse } from '@/types'
|
||||
import { useWhiteboardStore } from '@/stores/whiteboards'
|
||||
import { useWhiteboardsStore } from '@/stores/whiteboards'
|
||||
import { authService } from '@/services/authService'
|
||||
|
||||
const ACCESS_TOKEN = 'auth_token'
|
||||
@@ -129,7 +129,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
async function logout(allDevices = false) {
|
||||
const whiteboardStore = useWhiteboardStore()
|
||||
const whiteboardStore = useWhiteboardsStore()
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
import type { Whiteboard } from '@/types'
|
||||
import { whiteboardService } from '@/services/whiteboardService'
|
||||
|
||||
export const useWhiteboardStore = defineStore('whiteboards', () => {
|
||||
export const useWhiteboardsStore = defineStore('whiteboards', () => {
|
||||
const ownedWhiteboards = ref<Whiteboard[]>([])
|
||||
const recentWhiteboards = ref<Whiteboard[]>([])
|
||||
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() {
|
||||
ownedWhiteboards.value = []
|
||||
recentWhiteboards.value = []
|
||||
@@ -45,6 +64,7 @@ export const useWhiteboardStore = defineStore('whiteboards', () => {
|
||||
error,
|
||||
getWhiteboardHistory: getWhiteboardHistory,
|
||||
getRecentWhiteboards: getRecentWhiteboards,
|
||||
createNewWhiteboard: createNewWhiteboard,
|
||||
clearWhiteboards: clearWhiteboards
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import WhiteboardHistorySidebar from '@/components/WhiteboardHistorySidebar.vue'
|
||||
import RecentWhiteboardsPanel from '@/components/RecentWhiteboardsPanel.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useWhiteboardsStore } from "@/stores/whiteboards.ts";
|
||||
|
||||
const auth = useAuthStore()
|
||||
const whiteboards = useWhiteboardsStore()
|
||||
const router = useRouter()
|
||||
|
||||
const joinCode = ref('')
|
||||
const whiteboardTitle = ref('')
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -80,7 +105,10 @@ const showCreateModal = ref(false)
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<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>
|
||||
|
||||
@@ -26,10 +26,13 @@ async function handleLeave() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="store.isLoading" class="d-flex justify-content-center align-items-center vh-100">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<div v-if="store.isLoading" class="d-flex flex-column justify-content-center align-items-center vh-100">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted fs-5 text-center">
|
||||
Please wait while your whiteboard is loading...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/hubs/': {
|
||||
'/hubs': {
|
||||
target: 'http://localhost:5039',
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
|
||||
Reference in New Issue
Block a user