front
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.8",
|
||||
"pinia": "^3.0.4",
|
||||
"signalr": "^2.4.3",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^5.0.1",
|
||||
},
|
||||
@@ -547,6 +548,8 @@
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jquery": ["jquery@4.0.0", "", {}, "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
@@ -739,6 +742,8 @@
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"signalr": ["signalr@2.4.3", "", { "dependencies": { "jquery": ">=1.6.4" } }, "sha512-RbBKFVCZvDgyyxZDeu6Yck9T+diZO07GB0bDiKondUhBY1H8JRQSOq8R0pLkf47ddllQAssYlp7ckQAeom24mw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.8",
|
||||
"pinia": "^3.0.4",
|
||||
"signalr": "^2.4.3",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^5.0.1"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import AppTopBar from './components/AppTopBar.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
auth.initialize()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<AppTopBar />
|
||||
<main class="container py-4">
|
||||
|
||||
@@ -6,7 +6,7 @@ const auth = useAuthStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark border-bottom border-secondary">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark border-bottom border-secondary sticky-top">
|
||||
<div class="container">
|
||||
<RouterLink class="navbar-brand" to="/">AIPS</RouterLink>
|
||||
|
||||
|
||||
44
front/src/components/RecentWhiteboardsItem.vue
Normal file
44
front/src/components/RecentWhiteboardsItem.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { Whiteboard } from '@/types'
|
||||
|
||||
const props = defineProps<{ whiteboard: Whiteboard }>()
|
||||
const emit = defineEmits<{ click: [whiteboard: Whiteboard] }>()
|
||||
|
||||
const formatDate = (date: string | Date) =>
|
||||
new Date(date).toLocaleDateString(
|
||||
(navigator.languages && navigator.languages[0]) || '',
|
||||
{ day: '2-digit', month: '2-digit', year: 'numeric' }
|
||||
)
|
||||
|
||||
const handleClick = () => emit('click', props.whiteboard)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card border rounded-3 p-3 cursor-pointer hover-card"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="mb-0 text-dark">{{ whiteboard.title }}</h5>
|
||||
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hover-card:hover {
|
||||
border-color: #007bff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
</style>
|
||||
39
front/src/components/RecentWhiteboardsList.vue
Normal file
39
front/src/components/RecentWhiteboardsList.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useWhiteboardStore } from '@/stores/whiteboards'
|
||||
import RecentWhiteboardsItem from './RecentWhiteboardsItem.vue'
|
||||
|
||||
const store = useWhiteboardStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (store.recentWhiteboards.length === 0) store.getRecentWhiteboards()
|
||||
})
|
||||
|
||||
const sortedWhiteboards = computed(() =>
|
||||
[...store.recentWhiteboards].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
)
|
||||
|
||||
const handleClick = (whiteboard: any) => {
|
||||
console.log('Clicked:', whiteboard)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="d-flex flex-column gap-3 overflow-auto h-100 w-100 p-3" v-if="sortedWhiteboards.length > 0">
|
||||
<RecentWhiteboardsItem
|
||||
v-for="wb in sortedWhiteboards"
|
||||
:key="wb.id"
|
||||
:whiteboard="wb"
|
||||
@click="handleClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3 overflow-auto h-100 w-100 p-3" v-else>
|
||||
<p class="text-muted">No recent whiteboards</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
27
front/src/components/RecentWhiteboardsPanel.vue
Normal file
27
front/src/components/RecentWhiteboardsPanel.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import RecentWhiteboardsList from './RecentWhiteboardsList.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="whiteboards-panel card border-secondary shadow-sm h-100">
|
||||
<div class="card-header bg-light text-dark">
|
||||
Recent Whiteboards
|
||||
</div>
|
||||
<div class="card-body p-0 overflow-auto">
|
||||
<RecentWhiteboardsList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.whiteboards-panel {
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
44
front/src/components/WhiteboardHistoryItem.vue
Normal file
44
front/src/components/WhiteboardHistoryItem.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { Whiteboard } from '@/types'
|
||||
|
||||
const props = defineProps<{ whiteboard: Whiteboard }>()
|
||||
const emit = defineEmits<{ click: [whiteboard: Whiteboard] }>()
|
||||
|
||||
const formatDate = (date: string | Date) =>
|
||||
new Date(date).toLocaleDateString(
|
||||
(navigator.languages && navigator.languages[0]) || '',
|
||||
{ day: '2-digit', month: '2-digit', year: 'numeric' }
|
||||
)
|
||||
|
||||
const handleClick = () => emit('click', props.whiteboard)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card border rounded-3 p-3 cursor-pointer hover-card"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="mb-0 text-dark">{{ whiteboard.title }}</h5>
|
||||
<small class="text-muted">{{ formatDate(whiteboard.createdAt) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hover-card:hover {
|
||||
border-color: #007bff !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
</style>
|
||||
35
front/src/components/WhiteboardHistoryList.vue
Normal file
35
front/src/components/WhiteboardHistoryList.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useWhiteboardStore } from '@/stores/whiteboards'
|
||||
import WhiteboardHistoryItem from './WhiteboardHistoryItem.vue'
|
||||
|
||||
const store = useWhiteboardStore()
|
||||
|
||||
onMounted(() => {
|
||||
if (store.ownedWhiteboards.length === 0) store.getWhiteboardHistory()
|
||||
})
|
||||
|
||||
const sortedWhiteboards = computed(() =>
|
||||
[...store.ownedWhiteboards].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
)
|
||||
|
||||
const handleClick = (whiteboard: any) => {
|
||||
console.log('Clicked:', whiteboard)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column gap-3 overflow-auto h-100 w-100 p-3" v-if="sortedWhiteboards.length > 0">
|
||||
<WhiteboardHistoryItem
|
||||
v-for="wb in sortedWhiteboards"
|
||||
:key="wb.id"
|
||||
:whiteboard="wb"
|
||||
@click="handleClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3 overflow-auto h-100 w-100 p-3" v-else>
|
||||
<p class="text-muted">You have not created a whiteboard yet</p>
|
||||
</div>
|
||||
</template>
|
||||
58
front/src/components/WhiteboardHistorySidebar.vue
Normal file
58
front/src/components/WhiteboardHistorySidebar.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import WhiteboardHistoryList from './WhiteboardHistoryList.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<button
|
||||
class="btn btn-dark position-fixed top-50 start-0 translate-middle-y rounded-0 rounded-end py-3 px-2"
|
||||
type="button"
|
||||
data-bs-toggle="offcanvas"
|
||||
data-bs-target="#whiteboardSidebar"
|
||||
aria-controls="whiteboardSidebar"
|
||||
style="z-index: 1040; writing-mode: vertical-rl;"
|
||||
>
|
||||
My Whiteboards
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="whiteboardSidebar"
|
||||
class="offcanvas offcanvas-start bg-dark text-light"
|
||||
tabindex="-1"
|
||||
aria-labelledby="whiteboardSidebarLabel"
|
||||
>
|
||||
<div class="offcanvas-header border-bottom border-secondary">
|
||||
<h5 id="whiteboardSidebarLabel" class="offcanvas-title">
|
||||
My Whiteboards
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close btn-close-white"
|
||||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"
|
||||
/>
|
||||
</div>
|
||||
<div class="offcanvas-body p-0">
|
||||
<WhiteboardHistoryList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
#whiteboardSidebar.offcanvas-start {
|
||||
top: 56px;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#whiteboardSidebar.offcanvas-start {
|
||||
top: 56px;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
11
front/src/enums/index.ts
Normal file
11
front/src/enums/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum WhiteboardJoinPolicy {
|
||||
FreeToJoin,
|
||||
RequestToJoin,
|
||||
Private
|
||||
}
|
||||
|
||||
export enum WhiteboardState {
|
||||
Active,
|
||||
Inactive,
|
||||
Deleted
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import './assets/scss/main.scss'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
27
front/src/services/whiteboardService.ts
Normal file
27
front/src/services/whiteboardService.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Whiteboard } from "@/types";
|
||||
import { api } from './api'
|
||||
|
||||
export const whiteboardService = {
|
||||
async getWhiteboardHistory(): Promise<Whiteboard[]> {
|
||||
const raw = await api.get<any[]>('/api/Whiteboard/history')
|
||||
return raw.map(mapWhiteboard)
|
||||
},
|
||||
|
||||
async getRecentWhiteboards(): Promise<Whiteboard[]> {
|
||||
const raw = await api.get<any[]>('/api/Whiteboard/recent')
|
||||
return raw.map(mapWhiteboard)
|
||||
}
|
||||
}
|
||||
|
||||
function mapWhiteboard(raw: any): Whiteboard {
|
||||
return {
|
||||
id: raw.id,
|
||||
ownerId: raw.ownerId,
|
||||
title: raw.title,
|
||||
createdAt: new Date(raw.createdAt),
|
||||
deletedAt: raw.deletedAt ? new Date(raw.deletedAt) : undefined,
|
||||
maxParticipants: raw.maxParticipants,
|
||||
joinPolicy: raw.joinPolicy,
|
||||
state: raw.state,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +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 { authService } from '@/services/authService'
|
||||
|
||||
const ACCESS_TOKEN = 'auth_token'
|
||||
@@ -14,7 +15,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
// single-flight promise for refresh to avoid concurrent refresh requests
|
||||
let refreshPromise: Promise<void> | null = null
|
||||
|
||||
function setTokens(access: string | null, refresh: string | null) {
|
||||
@@ -28,28 +28,23 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (refresh) {
|
||||
localStorage.setItem(REFRESH_TOKEN, refresh)
|
||||
} else if (refresh === null) {
|
||||
// explicit null means remove
|
||||
localStorage.removeItem(REFRESH_TOKEN)
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRefresh(): Promise<void> {
|
||||
// if a refresh is already in progress, return that promise
|
||||
if (refreshPromise) return refreshPromise
|
||||
|
||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN)
|
||||
if (!refreshToken) {
|
||||
// nothing to do
|
||||
throw new Error('No refresh token')
|
||||
}
|
||||
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const res = await authService.refreshLogin(refreshToken)
|
||||
// API expected to return { accessToken, refreshToken }
|
||||
setTokens(res.accessToken ?? null, res.refreshToken ?? null)
|
||||
} finally {
|
||||
// clear so subsequent calls can create a new promise
|
||||
refreshPromise = null
|
||||
}
|
||||
})()
|
||||
@@ -70,7 +65,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isLoading.value = false
|
||||
return
|
||||
} catch (e) {
|
||||
// token might be expired; try refresh if we have refresh token
|
||||
if (savedRefresh) {
|
||||
try {
|
||||
await tryRefresh()
|
||||
@@ -78,14 +72,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isLoading.value = false
|
||||
return
|
||||
} catch (err) {
|
||||
// refresh failed - fallthrough to clearing auth
|
||||
console.warn('Token refresh failed during initialize', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not authenticated or refresh failed
|
||||
|
||||
setTokens(null, null)
|
||||
user.value = null
|
||||
isLoading.value = false
|
||||
@@ -96,14 +89,14 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error.value = null
|
||||
try {
|
||||
const res: AuthResponse = await authService.login(credentials)
|
||||
// expect AuthResponse to have accessToken and refreshToken
|
||||
|
||||
setTokens(res.accessToken ?? null, res.refreshToken ?? null)
|
||||
|
||||
try {
|
||||
user.value = await authService.getMe()
|
||||
} catch (e) {
|
||||
console.error('Logged in but failed to fetch user profile', e)
|
||||
// keep tokens but clear user
|
||||
|
||||
user.value = null
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -124,7 +117,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
try {
|
||||
user.value = await authService.getMe()
|
||||
} catch (e) {
|
||||
// keep tokens but no profile
|
||||
|
||||
user.value = null
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -136,16 +129,17 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
async function logout(allDevices = false) {
|
||||
const whiteboardStore = useWhiteboardStore()
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
if (allDevices) await authService.logoutAll()
|
||||
else await authService.logout(localStorage.getItem(REFRESH_TOKEN)!)
|
||||
} catch (e) {
|
||||
// ignore network errors on logout
|
||||
console.warn('Logout request failed', e)
|
||||
} finally {
|
||||
setTokens(null, null)
|
||||
whiteboardStore.clearWhiteboards()
|
||||
user.value = null
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
50
front/src/stores/whiteboards.ts
Normal file
50
front/src/stores/whiteboards.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { Whiteboard } from '@/types'
|
||||
import { whiteboardService } from '@/services/whiteboardService'
|
||||
|
||||
export const useWhiteboardStore = defineStore('whiteboards', () => {
|
||||
const ownedWhiteboards = ref<Whiteboard[]>([])
|
||||
const recentWhiteboards = ref<Whiteboard[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function getWhiteboardHistory() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
ownedWhiteboards.value = await whiteboardService.getWhiteboardHistory()
|
||||
} catch (err: any) {
|
||||
error.value = err.message ?? 'Failed to load whiteboards'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getRecentWhiteboards() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
recentWhiteboards.value = await whiteboardService.getRecentWhiteboards()
|
||||
} catch (err: any) {
|
||||
error.value = err.message ?? 'Failed to load whiteboards'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearWhiteboards() {
|
||||
ownedWhiteboards.value = []
|
||||
recentWhiteboards.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
ownedWhiteboards: ownedWhiteboards,
|
||||
recentWhiteboards: recentWhiteboards,
|
||||
isLoading,
|
||||
error,
|
||||
getWhiteboardHistory: getWhiteboardHistory,
|
||||
getRecentWhiteboards: getRecentWhiteboards,
|
||||
clearWhiteboards: clearWhiteboards
|
||||
}
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import {type WhiteboardJoinPolicy, WhiteboardState} from "@/enums";
|
||||
|
||||
export interface User {
|
||||
username: string
|
||||
email: string
|
||||
@@ -18,3 +20,14 @@ export interface AuthResponse {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export interface Whiteboard {
|
||||
id: string
|
||||
ownerId: string
|
||||
title: string
|
||||
createdAt: Date
|
||||
deletedAt?: Date
|
||||
maxParticipants?: number
|
||||
joinPolicy?: WhiteboardJoinPolicy
|
||||
state: WhiteboardState
|
||||
}
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import WhiteboardHistorySidebar from '@/components/WhiteboardHistorySidebar.vue'
|
||||
import RecentWhiteboardsPanel from '@/components/RecentWhiteboardsPanel.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Home</h1>
|
||||
<WhiteboardHistorySidebar v-if="auth.isAuthenticated" />
|
||||
|
||||
<div class="container py-4">
|
||||
<div
|
||||
v-if="auth.isAuthenticated"
|
||||
class="d-flex justify-content-center position-absolute bottom-0 start-50 translate-middle-x mb-3 w-50"
|
||||
>
|
||||
<RecentWhiteboardsPanel />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user