diff --git a/frontend/blog_portal/src/components/PostCard.vue b/frontend/blog_portal/src/components/PostCard.vue new file mode 100644 index 0000000..d35de20 --- /dev/null +++ b/frontend/blog_portal/src/components/PostCard.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/frontend/blog_portal/src/stores/posts.ts b/frontend/blog_portal/src/stores/posts.ts new file mode 100644 index 0000000..a660f0e --- /dev/null +++ b/frontend/blog_portal/src/stores/posts.ts @@ -0,0 +1,124 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { apiDelete, apiGet, apiPost } from "@/api/client"; + +export interface PostSummary { + id: number; + userId: number; + authorDisplayName: string; + title: string; + body: string; + createdAt: string; + featureSlot: number | null; +} + +interface PostListPayload { + posts: PostSummary[]; +} + +interface PostPayload { + post: PostSummary; +} + +interface UserPostsPayload { + user: { id: number; displayName: string }; + posts: PostSummary[]; +} + +export const usePostsStore = defineStore("posts", () => { + const recent = ref([]); + const featured = ref([]); + const error = ref(null); + + async function fetchRecent(): Promise { + const result = await apiGet("/posts"); + if (result.ok && result.data) { + recent.value = result.data.posts; + } else { + error.value = result.error; + } + } + + async function fetchFeatured(): Promise { + const result = await apiGet("/posts/featured"); + if (result.ok && result.data) { + featured.value = result.data.posts; + } else { + error.value = result.error; + } + } + + async function fetchPost(id: number): Promise { + const result = await apiGet(`/posts/${id}`); + if (result.ok && result.data) return result.data.post; + error.value = result.error; + return null; + } + + async function fetchUserPosts(displayName: string): Promise { + const result = await apiGet( + `/users/${encodeURIComponent(displayName)}/posts`, + ); + if (result.ok && result.data) return result.data; + error.value = result.error; + return null; + } + + async function createPost(title: string, body: string): Promise { + const result = await apiPost("/posts", { title, body }); + if (result.ok && result.data) return result.data.post; + error.value = result.error; + return null; + } + + async function deletePost(id: number): Promise { + const result = await apiDelete(`/posts/${id}`); + if (!result.ok) { + error.value = result.error; + return false; + } + return true; + } + + async function feature(postId: number, slot: number): Promise { + const result = await apiPost("/admin/posts/feature", { + postId, + slot, + }); + if (!result.ok) { + error.value = result.error; + return false; + } + return true; + } + + async function unfeature(postId: number): Promise { + const result = await apiPost("/admin/posts/unfeature", { + postId, + }); + if (!result.ok) { + error.value = result.error; + return false; + } + return true; + } + + function clearError(): void { + error.value = null; + } + + return { + recent, + featured, + error, + fetchRecent, + fetchFeatured, + fetchPost, + fetchUserPosts, + createPost, + deletePost, + feature, + unfeature, + clearError, + }; +}); diff --git a/frontend/blog_portal/src/stores/users.ts b/frontend/blog_portal/src/stores/users.ts new file mode 100644 index 0000000..95085cb --- /dev/null +++ b/frontend/blog_portal/src/stores/users.ts @@ -0,0 +1,49 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { apiGet, apiPost } from "@/api/client"; + +export interface UserSearchResult { + id: number; + email: string; + displayName: string; + isAdmin: boolean; +} + +interface SearchPayload { + users: UserSearchResult[]; +} + +interface PromotePayload { + user: UserSearchResult; +} + +export const useUsersStore = defineStore("users", () => { + const results = ref([]); + const error = ref(null); + + async function search(query: string): Promise { + const trimmed = query.trim(); + if (trimmed === "") { + results.value = []; + return; + } + const result = await apiGet(`/users?q=${encodeURIComponent(trimmed)}`); + if (result.ok && result.data) { + results.value = result.data.users; + } else { + error.value = result.error; + results.value = []; + } + } + + async function promote(userId: number): Promise { + const result = await apiPost("/admin/users/promote", { userId }); + if (!result.ok) { + error.value = result.error; + return false; + } + return true; + } + + return { results, error, search, promote }; +}); diff --git a/frontend/blog_portal/src/views/HomePage.vue b/frontend/blog_portal/src/views/HomePage.vue index 5cd2868..f41f752 100644 --- a/frontend/blog_portal/src/views/HomePage.vue +++ b/frontend/blog_portal/src/views/HomePage.vue @@ -1,22 +1,117 @@