diff --git a/front/src/components/whiteboard/WhiteboardCanvas.vue b/front/src/components/whiteboard/WhiteboardCanvas.vue
new file mode 100644
index 0000000..cc5aafd
--- /dev/null
+++ b/front/src/components/whiteboard/WhiteboardCanvas.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
diff --git a/front/src/components/whiteboard/WhiteboardToolbar.vue b/front/src/components/whiteboard/WhiteboardToolbar.vue
new file mode 100644
index 0000000..904622c
--- /dev/null
+++ b/front/src/components/whiteboard/WhiteboardToolbar.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
diff --git a/front/src/components/whiteboard/shapes/SvgDraftRect.vue b/front/src/components/whiteboard/shapes/SvgDraftRect.vue
new file mode 100644
index 0000000..17632e1
--- /dev/null
+++ b/front/src/components/whiteboard/shapes/SvgDraftRect.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/front/src/components/whiteboard/shapes/SvgRectangle.vue b/front/src/components/whiteboard/shapes/SvgRectangle.vue
new file mode 100644
index 0000000..95aedb3
--- /dev/null
+++ b/front/src/components/whiteboard/shapes/SvgRectangle.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/front/src/services/whiteboardHubService.ts b/front/src/services/whiteboardHubService.ts
new file mode 100644
index 0000000..9945d81
--- /dev/null
+++ b/front/src/services/whiteboardHubService.ts
@@ -0,0 +1,49 @@
+import { SignalRService } from '@/services/signalr.ts'
+import type { Rectangle, Whiteboard } from '@/types/whiteboard.ts'
+
+const client = new SignalRService(`http://localhost:5039/whiteboardhub`)
+
+export const whiteboardHubService = {
+ async connect() {
+ await client.start()
+ },
+
+ async disconnect() {
+ await client.stop()
+ },
+
+ async joinWhiteboard(id: string) {
+ await client.invoke('JoinWhiteboard', id)
+ },
+
+ async leaveWhiteboard(id: string) {
+ await client.invoke('LeaveWhiteboard', id)
+ },
+
+ async addRectangle(rectangle: Rectangle) {
+ await client.invoke('AddRectangle', rectangle)
+ },
+
+ onInitWhiteboard(callback: (whiteboard: Whiteboard) => void) {
+ client.on('InitWhiteboard', callback)
+ },
+
+ onAddedRectangle(callback: (rectangle: Rectangle) => void) {
+ client.on('AddedRectangle', callback)
+ },
+
+ onJoined(callback: (userId: string) => void) {
+ client.on('Joined', callback)
+ },
+
+ onLeaved(callback: (userId: string) => void) {
+ client.on('Leaved', callback)
+ },
+
+ offAll() {
+ client.off('InitWhiteboard')
+ client.off('AddedRectangle')
+ client.off('Joined')
+ client.off('Leaved')
+ },
+}
diff --git a/front/src/stores/whiteboard.ts b/front/src/stores/whiteboard.ts
new file mode 100644
index 0000000..1a3881a
--- /dev/null
+++ b/front/src/stores/whiteboard.ts
@@ -0,0 +1,88 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+import type { Rectangle, ShapeTool, Whiteboard } from '@/types/whiteboard.ts'
+import { whiteboardHubService } from '@/services/whiteboardHubService.ts'
+
+export const useWhiteboardStore = defineStore('whiteboard', () => {
+ const whiteboard = ref(null)
+ const selectedTool = ref('rectangle')
+ const isConnected = ref(false)
+ const isLoading = ref(false)
+ const error = ref(null)
+
+ async function joinWhiteboard(id: string) {
+ isLoading.value = true
+ error.value = null
+
+ try {
+ await whiteboardHubService.connect()
+ isConnected.value = true
+
+ whiteboardHubService.onInitWhiteboard((wb) => {
+ whiteboard.value = wb
+ isLoading.value = false
+ })
+
+ whiteboardHubService.onAddedRectangle((rectangle) => {
+ whiteboard.value?.rectangles.push(rectangle)
+ })
+
+ whiteboardHubService.onJoined((userId) => {
+ console.log('User joined:', userId)
+ })
+
+ whiteboardHubService.onLeaved((userId) => {
+ console.log('User left:', userId)
+ })
+
+ await whiteboardHubService.joinWhiteboard(id)
+ } catch (e: any) {
+ error.value = e?.message ?? 'Failed to join whiteboard'
+ isLoading.value = false
+ }
+ }
+
+ async function leaveWhiteboard() {
+ if (!whiteboard.value) return
+
+ try {
+ await whiteboardHubService.leaveWhiteboard(whiteboard.value.whiteboardId)
+ } catch (e) {
+ console.warn('Leave request failed', e)
+ }
+
+ whiteboardHubService.offAll()
+ await whiteboardHubService.disconnect()
+
+ whiteboard.value = null
+ isConnected.value = false
+ selectedTool.value = 'rectangle'
+ error.value = null
+ }
+
+ async function addRectangle(rectangle: Rectangle) {
+ whiteboard.value?.rectangles.push(rectangle)
+
+ try {
+ await whiteboardHubService.addRectangle(rectangle)
+ } catch (e: any) {
+ console.error('Failed to send rectangle', e)
+ }
+ }
+
+ function selectTool(tool: ShapeTool) {
+ selectedTool.value = tool
+ }
+
+ return {
+ whiteboard,
+ selectedTool,
+ isConnected,
+ isLoading,
+ error,
+ joinWhiteboard,
+ leaveWhiteboard,
+ addRectangle,
+ selectTool,
+ }
+})
diff --git a/front/src/types/whiteboard.ts b/front/src/types/whiteboard.ts
new file mode 100644
index 0000000..85818ab
--- /dev/null
+++ b/front/src/types/whiteboard.ts
@@ -0,0 +1,42 @@
+export interface Position {
+ x: number
+ y: number
+}
+
+export interface Shape {
+ id: string
+ ownerId: string
+ position: Position
+ color: string
+}
+
+export interface Rectangle extends Shape {
+ endPosition: Position
+ borderThickness: number
+}
+
+export interface Arrow extends Shape {
+ endPosition: Position
+ thickness: number
+}
+
+export interface Line extends Shape {
+ endPosition: Position
+ thickness: number
+}
+
+export interface TextShape extends Shape {
+ textValue: string
+ textSize: number
+}
+
+export interface Whiteboard {
+ whiteboardId: string
+ ownerId: string
+ rectangles: Rectangle[]
+ arrows: Arrow[]
+ lines: Line[]
+ textShapes: TextShape[]
+}
+
+export type ShapeTool = 'rectangle' | 'arrow' | 'line' | 'text'
diff --git a/front/src/views/WhiteboardView.vue b/front/src/views/WhiteboardView.vue
new file mode 100644
index 0000000..d9ff4f0
--- /dev/null
+++ b/front/src/views/WhiteboardView.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ {{ store.error }}
+
+
+
+
+
+
+
+