created frontend with login, room listing and creation, and message page with all features

This commit is contained in:
2025-12-15 14:50:50 +01:00
parent f10c761f1b
commit d6a26c0d09
18 changed files with 653 additions and 165 deletions

View File

@@ -10,15 +10,17 @@
"tauri": "tauri"
},
"dependencies": {
"vue": "^3.5.13",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
"@tauri-apps/plugin-opener": "^2",
"pinia": "^3.0.4",
"vue": "^3.5.13",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"vite": "^6.0.3",
"vue-tsc": "^2.1.10",
"@tauri-apps/cli": "^2"
"vue-tsc": "^2.1.10"
}
}

View File

@@ -1,160 +1,10 @@
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { useAuthStore } from "./stores/auth";
const greetMsg = ref("");
const name = ref("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsg.value = await invoke("greet", { name: name.value });
}
const auth = useAuthStore();
</script>
<template>
<main class="container">
<h1>Welcome to Tauri + Vue</h1>
<div class="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and Vue logos to learn more.</p>
<form class="row" @submit.prevent="greet">
<input id="greet-input" v-model="name" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p>{{ greetMsg }}</p>
</main>
<!-- You can later replace this with a real layout -->
<router-view />
</template>
<style scoped>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #249b73);
}
</style>
<style>
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
</style>

27
src/api/client.ts Normal file
View File

@@ -0,0 +1,27 @@
import { useAuthStore } from '../stores/auth'
const BASE_URL = 'http://localhost:8080'
export async function apiFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const auth = useAuthStore()
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
...options.headers,
},
})
if (!res.ok) {
const text = await res.text()
throw new Error(text || res.statusText)
}
return res.json()
}

17
src/api/messages.ts Normal file
View File

@@ -0,0 +1,17 @@
import { apiFetch } from './client'
import type { Message } from '../types/api'
export function fetchMessages(roomUuid: string) {
return apiFetch<Message[]>(`/messages/${roomUuid}`)
}
export function sendMessage(roomUuid: string, content: string) {
return apiFetch<Message>(`/messages/${roomUuid}`, {
method: 'POST',
body: JSON.stringify({
message_type: 'text',
content,
}),
})
}

14
src/api/rooms.ts Normal file
View File

@@ -0,0 +1,14 @@
import { apiFetch } from './client'
import type { Room } from '../types/api'
export function fetchRooms(userUuid: string) {
return apiFetch<Room[]>(`/rooms/${userUuid}`)
}
export function createRoom(name: string) {
return apiFetch<Room>('/rooms', {
method: 'POST',
body: JSON.stringify({ name }),
})
}

73
src/base.css Normal file
View File

@@ -0,0 +1,73 @@
/* ---- CSS reset (minimal, modern) ---- */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
}
html,
body,
#app {
height: 100%;
}
/* ---- Theme variables ---- */
:root {
--bg: #0f1115;
--panel: #171923;
--text: #e6e6eb;
--muted: #9aa0a6;
--accent: #f27aa2;
--accent-hover: #ff91b4;
--border: #2a2f3a;
--radius: 8px;
}
/* ---- Base ---- */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.5;
}
/* ---- Inputs & buttons ---- */
input, textarea {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
padding: 0.6rem 0.7rem;
border-radius: var(--radius);
font-size: 0.95rem;
}
input::placeholder, textarea::placeholder {
color: var(--muted);
}
input:focus, textarea:focus {
outline: none;
border-color: var(--accent);
}
button {
font-size: 1rem;
background: var(--accent);
color: #0b0d12;
border: none;
border-radius: var(--radius);
padding: 0.6rem 1rem;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: var(--accent-hover);
}

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ref } from 'vue'
import { createRoom } from '../api/rooms'
import type { Room } from '../types/api'
const name = ref('')
const emit = defineEmits<{ (e: 'created', room: Room): void }>()
async function submit() {
const room = await createRoom(name.value)
emit('created', room)
name.value = ''
}
</script>
<template>
<div>
<input v-model="name" placeholder="room name" />
<button @click="submit">Create</button>
</div>
</template>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const content = ref('')
const emit = defineEmits<{ (e: 'send', content: string): void }>()
function submit() {
if (!content.value) return
emit('send', content.value)
content.value = ''
resize()
}
function resize() {
nextTick(() => {
const textarea = document.querySelector('textarea')
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
}
})
}
function handleKeydown(e: KeyboardEvent) {
if ((e.shiftKey || e.altKey) && e.key === 'Enter') {
// Insert a line break at cursor
const textarea = e.target as HTMLTextAreaElement
const start = textarea.selectionStart
const end = textarea.selectionEnd
content.value = content.value.substring(0, start) + '\n' + content.value.substring(end)
nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = start + 1
})
resize()
e.preventDefault()
} else if (!e.shiftKey && !e.altKey && e.key === 'Enter') {
submit()
e.preventDefault()
}
}
</script>
<template>
<textarea v-model="content" @input="resize" @keydown="handleKeydown" placeholder="type a message" rows="1"></textarea>
</template>
<style scoped>
textarea {
width: 100%;
resize: none;
overflow: hidden;
padding: 0.5rem;
font-size: 1rem;
line-height: 1.4;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { Message } from '../types/api'
defineProps<{ messages: Message[] }>()
</script>
<template>
<ul>
<li v-for="(m, i) in messages" :key="i">
<strong>{{ m.sender }}:</strong> {{ m.content }}
</li>
</ul>
</template>
<style scoped>
ul {
padding: 0;
margin: 0;
list-style: none;
word-wrap: break-word;
}
li {
white-space: pre-wrap;
}
.message-content {
display: inline-block;
max-width: 100%;
word-break: break-word;
}
</style>

View File

@@ -1,4 +1,11 @@
import { createApp } from "vue";
import App from "./App.vue";
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
createApp(App).mount("#app");
import './base.css'
createApp(App)
.use(createPinia())
.use(router)
.mount('#app')

78
src/pages/ChatPage.vue Normal file
View File

@@ -0,0 +1,78 @@
<template>
<div class="chat-page">
<div class="messages-container" ref="messageListRef">
<MessageList :messages="messages" />
</div>
<div class="input-container">
<MessageInput @send="onSend" />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, nextTick } from "vue";
import { fetchMessages, sendMessage } from "../api/messages";
import type { Message } from "../types/api";
import MessageList from "../components/MessageList.vue";
import MessageInput from "../components/MessageInput.vue";
const props = defineProps<{ uuid: string }>();
const messages = ref<Message[]>([]);
async function load() {
messages.value = await fetchMessages(props.uuid);
}
async function onSend(content: string) {
const msg = await sendMessage(props.uuid, content);
messages.value.push(msg);
await nextTick();
scrollToBottom();
}
onMounted(async () => {
await load();
await nextTick();
scrollToBottom();
});
const messageListRef = ref<HTMLElement | null>(null);
function scrollToBottom() {
if (messageListRef.value) {
messageListRef.value.scrollTop = messageListRef.value.scrollHeight;
}
}
</script>
<style scoped>
.chat-page {
display: flex;
flex-direction: column;
height: 100%;
max-width: 720px;
margin: 0 auto;
padding: 15px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--panel);
overflow: hidden;
}
.messages-container {
flex: 1;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
word-wrap: break-word;
}
.input-container {
padding: 0.5rem 1rem;
border-top: 1px solid var(--border);
background: var(--panel);
}
</style>

58
src/pages/LoginPage.vue Normal file
View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref } from "vue";
import { useAuthStore } from "../stores/auth";
import { useRouter } from "vue-router";
const email = ref("");
const username = ref("");
const password = ref("");
const auth = useAuthStore();
const router = useRouter();
async function submit() {
await auth.login(email.value, username.value, password.value);
router.push("/");
}
</script>
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit">
<h1>Login</h1>
<input v-model="email" placeholder="email" />
<input v-model="password" type="password" placeholder="password" />
<button type="submit">Login</button>
</form>
</div>
</template>
<style scoped>
.login-page {
height: 100%;
display: grid;
place-items: center;
}
.login-card {
width: 100%;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.login-card h1 {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
}
</style>

83
src/pages/RoomsPage.vue Normal file
View File

@@ -0,0 +1,83 @@
<template>
<div class="rooms-page">
<header class="rooms-header">
<h1>Your rooms</h1>
<CreateRoomForm @created="rooms.push($event)" />
</header>
<ul class="rooms-list">
<li v-for="room in rooms" :key="room.uuid" class="room-item">
<router-link class="room-link" :to="`/rooms/${room.uuid}`">
{{ room.name }}
</router-link>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAuthStore } from '../stores/auth'
import { fetchRooms } from '../api/rooms'
import type { Room } from '../types/api'
const auth = useAuthStore()
const rooms = ref<Room[]>([])
onMounted(async () => {
rooms.value = await fetchRooms(auth.uuid!)
})
</script>
<script lang="ts">
import CreateRoomForm from '../components/CreateRoomForm.vue'
export default { components: { CreateRoomForm } }
</script>
<style scoped>
.rooms-page {
max-width: 720px;
margin: 0 auto;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.rooms-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.rooms-header h1 {
font-size: 1.5rem;
font-weight: 600;
}
.rooms-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.room-item {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.room-link {
display: block;
padding: 0.75rem 1rem;
color: var(--text);
text-decoration: none;
}
.room-link:hover {
background: rgba(255, 255, 255, 0.03);
}
</style>

25
src/router/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import LoginPage from '../pages/LoginPage.vue'
import RoomsPage from '../pages/RoomsPage.vue'
import ChatPage from '../pages/ChatPage.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: LoginPage },
{ path: '/', component: RoomsPage },
{ path: '/rooms/:uuid', component: ChatPage, props: true },
],
})
router.beforeEach((to) => {
const auth = useAuthStore()
if (!auth.isAuthenticated && to.path !== '/login') {
return '/login'
}
})
export default router

32
src/stores/auth.ts Normal file
View File

@@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
import type { LoginResponse } from '../types/api'
import { apiFetch } from '../api/client'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: null as string | null,
uuid: null as string | null,
}),
getters: {
isAuthenticated: (s) => !!s.token,
},
actions: {
async login(email: string, username: string, password: string) {
const res = await apiFetch<LoginResponse>('/login', {
method: 'POST',
body: JSON.stringify({ email, username, password }),
})
this.token = res.token
this.uuid = res.uuid
},
logout() {
this.token = null
this.uuid = null
},
},
})

17
src/types/api.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface LoginResponse {
uuid: string
token: string
}
export interface Room {
uuid: string
owner: number
name: string
}
export interface Message {
sender: string
message_type: 'text'
content: string
}

View File

@@ -19,10 +19,10 @@ export default defineConfig(async () => ({
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`

View File

@@ -437,6 +437,38 @@
de-indent "^1.0.2"
he "^1.2.0"
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/devtools-api@^7.7.7":
version "7.7.9"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063"
integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==
dependencies:
"@vue/devtools-kit" "^7.7.9"
"@vue/devtools-kit@^7.7.9":
version "7.7.9"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58"
integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==
dependencies:
"@vue/devtools-shared" "^7.7.9"
birpc "^2.3.0"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^1.0.0"
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-shared@^7.7.9":
version "7.7.9"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14"
integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==
dependencies:
rfdc "^1.4.1"
"@vue/language-core@2.2.12":
version "2.2.12"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.12.tgz#d01f7e865f593f968cb65c12a13d8337e65641f0"
@@ -499,6 +531,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
birpc@^2.3.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145"
integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==
brace-expansion@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
@@ -506,6 +543,13 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
copy-anything@^4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
dependencies:
is-what "^5.2.0"
csstype@^3.1.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
@@ -573,6 +617,16 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
is-what@^5.2.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
@@ -587,6 +641,11 @@ minimatch@^9.0.3:
dependencies:
brace-expansion "^2.0.1"
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
@@ -602,6 +661,11 @@ path-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
perfect-debounce@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -612,6 +676,13 @@ picomatch@^4.0.2, picomatch@^4.0.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pinia@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.4.tgz#75dde12784a61e34c1fa6abcd13c1a1061c360c0"
integrity sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==
dependencies:
"@vue/devtools-api" "^7.7.7"
postcss@^8.5.3, postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
@@ -621,6 +692,11 @@ postcss@^8.5.3, postcss@^8.5.6:
picocolors "^1.1.1"
source-map-js "^1.2.1"
rfdc@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rollup@^4.34.9:
version "4.53.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.4.tgz#5517de2593624928ac18f041b269f3b79cb64e09"
@@ -657,6 +733,18 @@ source-map-js@^1.2.1:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
speakingurl@^14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
superjson@^2.2.2:
version "2.2.6"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099"
integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==
dependencies:
copy-anything "^4"
tinyglobby@^0.2.13:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
@@ -689,6 +777,13 @@ vscode-uri@^3.0.8:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-router@^4.6.4:
version "4.6.4"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-tsc@^2.1.10:
version "2.2.12"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.12.tgz#5f719b08ef7390a763c1a20169ca5c9d09d55688"