Extract Vue frontend into separate container and add stp_wasm crate
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m58s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m58s
Move Vue app from nginx/vue/ to top-level vue/ with its own Dockerfile, update docker-compose configs and nginx proxy to serve from the new container, and add initial Rust WASM crate (stp_wasm). Also fix .gitignore to exclude Rust target/ directories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
36
vue/src/stores/activity.js
Normal file
36
vue/src/stores/activity.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const activity_template = {
|
||||
type: "activity",
|
||||
name: "nameof",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
export const useActivityStore = defineStore("activity", () => {
|
||||
const activity = ref([activity_template]);
|
||||
|
||||
const activityCount = computed(() => activity.value.length);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.activities,
|
||||
(newActivities) => {
|
||||
if (newActivities.length > 0) {
|
||||
activity.value = newActivities;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchActivity() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
return {
|
||||
activity,
|
||||
activityCount,
|
||||
fetchActivity,
|
||||
};
|
||||
});
|
||||
90
vue/src/stores/auth.js
Normal file
90
vue/src/stores/auth.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const user = ref({});
|
||||
const loggedIn = computed(() => !!user.value.username);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.me,
|
||||
(me) => {
|
||||
if (me) {
|
||||
user.value = me;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function logOut() {
|
||||
try {
|
||||
await gql(`mutation { logout }`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
user.value = {};
|
||||
}
|
||||
|
||||
async function logIn(username, password) {
|
||||
try {
|
||||
const data = await gql(
|
||||
`mutation Login($input: LoginInput!) { login(input: $input) { user { id username admin } } }`,
|
||||
{ input: { username, password } },
|
||||
);
|
||||
user.value = data.login.user;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser(username, password) {
|
||||
try {
|
||||
const data = await gql(
|
||||
`mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id username admin } }`,
|
||||
{ input: { username, password } },
|
||||
);
|
||||
return data.createUser;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
try {
|
||||
const data = await gql(
|
||||
`mutation { refreshToken { user { id username admin } } }`,
|
||||
);
|
||||
user.value = data.refreshToken.user;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function setUserAdmin(userId, admin) {
|
||||
try {
|
||||
const data = await gql(
|
||||
`mutation SetUserAdmin($id: ID!, $admin: Boolean!) { setUserAdmin(id: $id, admin: $admin) { id username admin } }`,
|
||||
{ id: userId, admin },
|
||||
);
|
||||
return data.setUserAdmin;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
|
||||
loggedIn,
|
||||
|
||||
logIn,
|
||||
refreshToken,
|
||||
logOut,
|
||||
createUser,
|
||||
setUserAdmin,
|
||||
};
|
||||
});
|
||||
36
vue/src/stores/favorites.js
Normal file
36
vue/src/stores/favorites.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const favorite_template = {
|
||||
type: "favorite",
|
||||
name: "nameof",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
export const useFavoritesStore = defineStore("favorites", () => {
|
||||
const favorites = ref([favorite_template]);
|
||||
|
||||
const favoritesCount = computed(() => favorites.value.length);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.favorites,
|
||||
(newFavorites) => {
|
||||
if (newFavorites.length > 0) {
|
||||
favorites.value = newFavorites;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchFavorites() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
return {
|
||||
favorites,
|
||||
favoritesCount,
|
||||
fetchFavorites,
|
||||
};
|
||||
});
|
||||
74
vue/src/stores/homeData.js
Normal file
74
vue/src/stores/homeData.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import axios from "axios";
|
||||
|
||||
export const useHomeDataStore = defineStore("homeData", () => {
|
||||
const loaded = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const me = ref(null);
|
||||
const posts = ref([]);
|
||||
const favorites = ref([]);
|
||||
const activities = ref([]);
|
||||
const spotifyRecent = ref([]);
|
||||
const rowingSessions = ref([]);
|
||||
const gitFeed = ref(null);
|
||||
const radioLive = ref(false);
|
||||
|
||||
async function fetchAll() {
|
||||
try {
|
||||
const [data] = await Promise.all([
|
||||
gql(`
|
||||
query HomeData {
|
||||
posts { id title content createdAt updatedAt author { id username } }
|
||||
favorites { id type name link createdAt }
|
||||
activities { id type name link createdAt }
|
||||
spotifyRecent { track { name album { name images { url } } artists { name } } playedAt }
|
||||
rowingSessions { id date time distance timePer500m calories }
|
||||
giteaFeed { avatarUrl repoUrl repoName opType commitMessage createdAt }
|
||||
me { id username admin }
|
||||
}
|
||||
`),
|
||||
fetchRadioStatus(),
|
||||
]);
|
||||
posts.value = data.posts;
|
||||
favorites.value = data.favorites;
|
||||
activities.value = data.activities;
|
||||
spotifyRecent.value = data.spotifyRecent;
|
||||
rowingSessions.value = data.rowingSessions;
|
||||
gitFeed.value = data.giteaFeed || null;
|
||||
me.value = data.me || null;
|
||||
loaded.value = true;
|
||||
} catch (err) {
|
||||
console.error("HomeData fetch failed:", err);
|
||||
error.value = err;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRadioStatus() {
|
||||
try {
|
||||
await axios.head("/radio/stream");
|
||||
radioLive.value = true;
|
||||
} catch {
|
||||
radioLive.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
fetchAll();
|
||||
|
||||
return {
|
||||
loaded,
|
||||
error,
|
||||
me,
|
||||
posts,
|
||||
favorites,
|
||||
activities,
|
||||
spotifyRecent,
|
||||
rowingSessions,
|
||||
gitFeed,
|
||||
radioLive,
|
||||
fetchAll,
|
||||
fetchRadioStatus,
|
||||
};
|
||||
});
|
||||
103
vue/src/stores/messages.js
Normal file
103
vue/src/stores/messages.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
function getWebSocketURL() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}/api/ws`;
|
||||
}
|
||||
|
||||
export const useMessagesStore = defineStore("messages", () => {
|
||||
const socket = ref(null);
|
||||
const messages = ref([]);
|
||||
const isConnected = ref(false);
|
||||
const lastError = ref(null);
|
||||
let intentionalClose = false;
|
||||
let reconnectDelay = 1000;
|
||||
let reconnectTimer = null;
|
||||
|
||||
const messagesCount = computed(() => messages.value.length);
|
||||
|
||||
function connect() {
|
||||
if (socket.value && isConnected.value) return;
|
||||
intentionalClose = false;
|
||||
|
||||
socket.value = new WebSocket(getWebSocketURL());
|
||||
|
||||
socket.value.onopen = () => {
|
||||
isConnected.value = true;
|
||||
lastError.value = null;
|
||||
reconnectDelay = 1000;
|
||||
messages.value = [];
|
||||
};
|
||||
|
||||
socket.value.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
messages.value.push(data);
|
||||
} catch {
|
||||
messages.value.push({ text: event.data });
|
||||
}
|
||||
};
|
||||
|
||||
socket.value.onerror = (error) => {
|
||||
lastError.value = error;
|
||||
};
|
||||
|
||||
socket.value.onclose = () => {
|
||||
isConnected.value = false;
|
||||
socket.value = null;
|
||||
if (!intentionalClose) {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
intentionalClose = true;
|
||||
clearTimeout(reconnectTimer);
|
||||
if (!socket.value) return;
|
||||
socket.value.close();
|
||||
socket.value = null;
|
||||
isConnected.value = false;
|
||||
}
|
||||
|
||||
function sendMessage(text) {
|
||||
if (!socket.value || !isConnected.value) return;
|
||||
socket.value.send(JSON.stringify({ text }));
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
messages.value = [];
|
||||
}
|
||||
|
||||
async function uploadAndSendFile(file) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const res = await axios.post("/api/messages/upload", formData);
|
||||
const { url } = res.data;
|
||||
if (!socket.value || !isConnected.value) return;
|
||||
socket.value.send(JSON.stringify({ text: "", fileUrl: url }));
|
||||
} catch (err) {
|
||||
lastError.value = err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
isConnected,
|
||||
lastError,
|
||||
|
||||
messagesCount,
|
||||
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
clearMessages,
|
||||
uploadAndSendFile,
|
||||
};
|
||||
});
|
||||
48
vue/src/stores/models.js
Normal file
48
vue/src/stores/models.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export class User {
|
||||
constructor({ id, createdAt, updatedAt, deletedAt, username, admin }) {
|
||||
this.id = id;
|
||||
this.createdAt = new Date(createdAt);
|
||||
this.updatedAt = new Date(updatedAt);
|
||||
this.deletedAt = deletedAt ? new Date(deletedAt) : null;
|
||||
this.username = username;
|
||||
this.admin = admin;
|
||||
}
|
||||
}
|
||||
|
||||
export class Message {
|
||||
constructor({ id, text, author, createdAt, deletedAt }) {
|
||||
this.id = id;
|
||||
this.content = text;
|
||||
this.author = author ? new User(author) : null;
|
||||
this.createdAt = new Date(createdAt);
|
||||
this.deletedAt = deletedAt ? new Date(deletedAt) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export class Post {
|
||||
constructor({
|
||||
id,
|
||||
title,
|
||||
author,
|
||||
authorID,
|
||||
content,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
}) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.authorID = authorID;
|
||||
this.author = author ? new User(author) : null;
|
||||
this.content = content;
|
||||
this.createdAt = new Date(createdAt);
|
||||
this.updatedAt = new Date(updatedAt);
|
||||
this.deletedAt = deletedAt ? new Date(deletedAt) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to parse posts from API
|
||||
export function parsePosts(postsArray) {
|
||||
if (!Array.isArray(postsArray)) return [];
|
||||
return postsArray.map((post) => new Post(post));
|
||||
}
|
||||
57
vue/src/stores/posts.js
Normal file
57
vue/src/stores/posts.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const post_template = {
|
||||
title: "Can't fetch from the db yo",
|
||||
content:
|
||||
"This is meant to be pulling from a database, but for some reason that isn't working and this is filler text that should hopefully never see the light of day. If you are reading this, something has gone horribly, horribly wrong. Please start crying and prepare for the incoming wrath of hell. Furthermore, this is very, very long because I am trying to test the scroll feature so thank you ^_^.",
|
||||
author: {
|
||||
username: "stp",
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
export const usePostsStore = defineStore("posts", () => {
|
||||
const posts = ref([post_template]);
|
||||
|
||||
const postsCount = computed(() => posts.value.length);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.posts,
|
||||
(newPosts) => {
|
||||
if (newPosts.length > 0) {
|
||||
posts.value = newPosts;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchPosts() {
|
||||
await homeData.fetchAll();
|
||||
}
|
||||
|
||||
async function deletePost(post) {
|
||||
try {
|
||||
await gql(
|
||||
`mutation DeletePost($id: ID!) { deletePost(id: $id) { id } }`,
|
||||
{ id: post.id },
|
||||
);
|
||||
console.log("Deleted:", post.id);
|
||||
await homeData.fetchAll();
|
||||
} catch (err) {
|
||||
console.error("Delete failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
posts,
|
||||
|
||||
postsCount,
|
||||
|
||||
fetchPosts,
|
||||
deletePost,
|
||||
};
|
||||
});
|
||||
59
vue/src/stores/songs.js
Normal file
59
vue/src/stores/songs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { gql } from "@/graphql";
|
||||
import { useHomeDataStore } from "@/stores/homeData";
|
||||
|
||||
const song_template = {
|
||||
track: {
|
||||
name: "^_^",
|
||||
album: { name: "", images: [{ url: "/img/Untitled.png" }] },
|
||||
artists: [{ name: ">_<" }],
|
||||
},
|
||||
};
|
||||
|
||||
export const useSongsStore = defineStore("songs", () => {
|
||||
const songs = ref([song_template]);
|
||||
|
||||
const songsCount = computed(() => songs.value.length);
|
||||
|
||||
const homeData = useHomeDataStore();
|
||||
watch(
|
||||
() => homeData.spotifyRecent,
|
||||
(newSongs) => {
|
||||
if (newSongs.length > 0) {
|
||||
songs.value = newSongs;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function fetchSongs() {
|
||||
try {
|
||||
const data = await gql(`
|
||||
query {
|
||||
spotifyRecent {
|
||||
track {
|
||||
name
|
||||
album { name images { url } }
|
||||
artists { name }
|
||||
}
|
||||
playedAt
|
||||
}
|
||||
}
|
||||
`);
|
||||
if (Array.isArray(data.spotifyRecent) && data.spotifyRecent.length > 0) {
|
||||
songs.value = data.spotifyRecent;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Cannot connect to Spotify API", err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
songs,
|
||||
|
||||
songsCount,
|
||||
|
||||
fetchSongs,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user