Skip to content

Commit

Permalink
Merge pull requests #96 and #95 into update-dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
floscher committed Jun 24, 2024
2 parents 53dec45 + 47ed2ca commit ce33119
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 138 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@material/web": "^1.5.0",
"@rushstack/eslint-patch": "^1.10.3",
"@tsconfig/node20": "^20.1.4",
"@types/bootstrap": "^5.2.10",
Expand Down
31 changes: 11 additions & 20 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@
<li class="nav-item">
<RouterLink to="/posts" class="nav-link">{{ t("nav.posts") }}</RouterLink>
</li>
<li v-if="isAdmin()" class="nav-item">
<li v-if="loggedInUserInfo?.permissions?.canEditUserRoles ?? false" class="nav-item">
<RouterLink to="/administration" class="nav-link">{{ t("nav.administration") }}</RouterLink>
</li>
<li v-if="appData.mainWebsite?.label" class="nav-item">
<a :href="appData.mainWebsite?.url" class="nav-link">{{ appData.mainWebsite?.label }}</a>
</li>
</ul>
<div class="username">
<login-button v-if="!loggedInUser"></login-button>
<user-name v-else :user="loggedInUser" @logout="logoutUser($event)"></user-name>
<login-button v-if="!loggedInUserInfo"></login-button>
<user-name v-else :user="loggedInUserInfo.user" @logout="logoutUser($event)"></user-name>
</div>
<light-dark-toggler @theme-changed="cssTheme = $event" style="margin-right: 2.5rem"></light-dark-toggler>
Expand All @@ -52,7 +52,7 @@
</div>
</nav>
<RouterView :userPermissions="userPermissions" />
<RouterView :userPermissions="loggedInUserInfo?.permissions" />
</div>
<footer class="page-footer">
<div class="container">
Expand Down Expand Up @@ -117,8 +117,7 @@ import { AuthEndpoints } from "@client/util/api-client.js";
import { saveIdToken } from "@client/util/storage.js";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
import type { AppSettingsDto, User, UserRolePermissionsType } from "@fumix/fu-blog-common";
import { permissionsForUser } from "@fumix/fu-blog-common";
import type { AppSettingsDto, LoggedInUserInfo } from "@fumix/fu-blog-common";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
Expand All @@ -127,8 +126,7 @@ const { t } = useI18n();
const route = useRoute();
const searchQuery = ref<string>("");
const router = useRouter();
const loggedInUser = ref<User | null>(null);
const userPermissions = ref<UserRolePermissionsType | null>(null);
const loggedInUserInfo = ref<LoggedInUserInfo | undefined>(undefined);
const cssTheme = ref<string | null>(null);
const appData: AppSettingsDto = (JSON.parse(document.getElementById("app-data")?.textContent ?? "{}") as AppSettingsDto) ?? {
Expand All @@ -153,13 +151,11 @@ const setOperator = (operator: string) => {
const setLoginUserAndPermissions = async () => {
AuthEndpoints.getLoggedInUser()
.then((oauthAccount) => {
loggedInUser.value = oauthAccount.user;
userPermissions.value = permissionsForUser(oauthAccount.user);
.then((userInfo: LoggedInUserInfo) => {
loggedInUserInfo.value = userInfo;
})
.catch(() => {
loggedInUser.value = null;
userPermissions.value = null;
loggedInUserInfo.value = undefined;
});
};
Expand All @@ -175,7 +171,7 @@ watch(route, async (value) => {
onMounted(() => {
// listen for token-changed event to gracefully handle login/logout
window.addEventListener("token-changed", (event) => {
if (!loggedInUser.value) {
if (!loggedInUserInfo.value) {
setLoginUserAndPermissions();
}
});
Expand All @@ -193,14 +189,9 @@ const startSearch = (search: string, operator: string = "and") => {
}
};
const isAdmin = () => {
return loggedInUser.value?.roles.includes("ADMIN");
};
const logoutUser = (event: Event) => {
saveIdToken(null); // clear localStorage
loggedInUser.value = null;
userPermissions.value = null;
loggedInUserInfo.value = undefined;
router.push(`/`);
};
</script>
6 changes: 5 additions & 1 deletion client/src/assets/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
}
}

.content,
.content,
.card,
.jumbotron {
transition: .75s ease-out all;
Expand Down Expand Up @@ -52,6 +52,10 @@ pre {
img {
max-width: 100%;
}
&.draft {
background: repeating-linear-gradient( -45deg, #fff, #fff 10px, #eee 10px, #eee 20px );
filter: grayscale(.5);
}
}

.jumbotron {
Expand Down
36 changes: 10 additions & 26 deletions client/src/components/DisplayTags.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,24 @@
<template>
<div class="badge-container">
<div
v-for="tag in [...tags].sort((a, b) => a.name.localeCompare(b.name))"
:key="tag.id"
class="badge me-1"
@click="searchWord(tag.name)"
>
{{ tag.name }}
</div>
</div>
<md-chip-set>
<md-suggestion-chip v-for="tag in sortedTags()" v-bind:key="tag" :label="tag" @click="searchWord(tag)"></md-suggestion-chip>
</md-chip-set>
</template>

<style lang="scss">
.badge-container {
padding: 0;
margin: 0;
.badge {
background-color: $badge-background-color;
color: $badge-text-color;
cursor: pointer;
}
}
</style>

<script setup lang="ts">
import type { Tag } from "@fumix/fu-blog-common";
import type { PropType } from "vue";
import { useRouter } from "vue-router";
import "@material/web/chips/chip-set.js";
import "@material/web/chips/filter-chip.js";
import "@material/web/chips/suggestion-chip.js";
const router = useRouter();
const props = defineProps({ tags: { type: Array as PropType<Tag[]>, required: true } });
const props = defineProps({ tags: { type: Array as PropType<string[]>, required: true } });
const sortedTags = () => [...props.tags].sort((a, b) => a.localeCompare(b));
const searchWord = (word: string): void => {
if (word) {
router.push(`/posts/?search=${word}&operator=and`);
router.push(`/posts/?search=${encodeURIComponent(word)}&operator=and`);
}
};
</script>
22 changes: 11 additions & 11 deletions client/src/components/PostPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<div class="row mb-2">
<div class="col">
<div class="card flex-md-row mb-4 box-shadow h-md-250">
<div class="card-body">
<div v-bind:class="{ 'card-body': true, draft: post.draft }">
<div class="clearfix mb-4">
<button v-if="props.userPermissions?.canDeletePost" class="btn btn-sm btn-danger float-end" @click="$emit('deletePost', post)">
<button v-if="props.post.permissions.canDelete" class="btn btn-sm btn-danger float-end" @click="$emit('deletePost', post)">
<fa-icon :icon="faTrash" />
{{ t("app.base.delete") }}
</button>
<button
v-if="props.userPermissions?.canEditPost"
v-if="props.post.permissions.canEdit"
class="btn btn-sm btn-secondary float-end mx-2"
@click="$emit('changePost', post)"
>
Expand All @@ -24,16 +24,16 @@

<p class="card-text my-4">{{ post.description }}</p>

<div v-if="post.createdAt" class="mb-1 text-muted creator">
<div v-if="post.created" class="mb-1 text-muted creator">
<fa-icon :icon="faClock" />
{{ $luxonDateTime.fromISO(post.createdAt.toString(), { locale: locale }).toRelativeCalendar() }}
<i v-if="post.createdBy">{{ post.createdBy.fullName }}</i>
{{ $luxonDateTime.fromISO(post.created.at.toString(), { locale: locale }).toRelativeCalendar() }}
<i v-if="post.created.by">{{ post.created.by }}</i>
</div>

<div v-if="post.updatedBy && post.updatedAt" class="mb-1 text-muted editor">
<div v-if="post.updated" class="mb-1 text-muted editor">
<fa-icon :icon="faEdit" />
{{ $luxonDateTime.fromISO(post.updatedAt.toString(), { locale: locale }).toRelativeCalendar() }}
<i>{{ post.updatedBy.fullName }}</i>
{{ $luxonDateTime.fromISO(post.updated.at.toString(), { locale: locale }).toRelativeCalendar() }}
<i v-if="post.updated.by">{{ post.updated.by }}</i>
</div>

<display-tags v-if="post.tags" :tags="post.tags"></display-tags>
Expand Down Expand Up @@ -71,7 +71,7 @@
import DisplayTags from "@client/components/DisplayTags.vue";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { faBookReader, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
import type { Post, UserRolePermissionsType } from "@fumix/fu-blog-common";
import type { Post, PublicPost, UserRolePermissionsType } from "@fumix/fu-blog-common";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
Expand All @@ -82,7 +82,7 @@ const locale = useI18n().locale.value;
const props = defineProps({
post: {
type: Object as PropType<Post>,
type: Object as PropType<PublicPost>,
required: true,
},
userPermissions: {
Expand Down
17 changes: 14 additions & 3 deletions client/src/util/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import type {
DraftResponseDto,
EditPostRequestDto,
JsonMimeType,
LoggedInUserInfo,
NewPostRequestDto,
OAuthAccount,
PublicPost,
SupportedImageMimeType,
} from "@fumix/fu-blog-common";
import { HttpHeader, imageBytesToDataUrl } from "@fumix/fu-blog-common";
Expand Down Expand Up @@ -86,10 +87,10 @@ function toFormData<T>(payload: ApiRequestJsonPayloadWithFiles<T> | null): FormD
}

export class AuthEndpoints {
static async getLoggedInUser(): Promise<OAuthAccount> {
static async getLoggedInUser(): Promise<LoggedInUserInfo> {
const token = loadIdToken();
if (token) {
return callServer<null, JsonMimeType, OAuthAccount>("/api/auth/loggedInUser/", "POST", "application/json");
return callServer<null, JsonMimeType, LoggedInUserInfo>("/api/auth/loggedInUser/", "POST", "application/json");
}
return Promise.reject();
}
Expand Down Expand Up @@ -138,4 +139,14 @@ export class PostEndpoints {
static async deletePost(id: number): Promise<{ affected: number }> {
return callServer<void, JsonMimeType, { affected: number }>(`/api/posts/delete/${id}`, "POST", "application/json");
}

static async findPosts(pageIndex: number, itemsPerPage = 12, search: string | undefined = undefined, operator: "and" | "or" = "and") {
return callServer<void, JsonMimeType, { data: [PublicPost[], number | null] }>(
`/api/posts/page/${pageIndex}/count/${itemsPerPage}${search ? `/search/${encodeURIComponent(search)}/operator/${operator}` : ""}###`,
"GET",
"application/json",
null,
true,
);
}
}
2 changes: 1 addition & 1 deletion client/src/views/PostView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<i>{{ post.updatedBy.fullName }}</i>
</div>
<display-tags v-if="post.tags" :tags="post.tags"></display-tags>
<display-tags v-if="post?.tags" :tags="post?.tags?.map((it) => it.name) ?? []"></display-tags>
<div v-html="post.sanitizedHtml" class="mt-4"></div>
</div>
Expand Down
40 changes: 19 additions & 21 deletions client/src/views/PostsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ import ConfirmDialog from "@client/components/ConfirmDialog.vue";
import LoadingSpinner from "@client/components/LoadingSpinner.vue";
import PostPreview from "@client/components/PostPreview.vue";
import WordCloud from "@client/components/WordCloud.vue";
import { PostEndpoints } from "@client/util/api-client";
import { faSadTear } from "@fortawesome/free-regular-svg-icons";
import { faAdd } from "@fortawesome/free-solid-svg-icons";
import type { ConfirmDialogData, Post, UserRolePermissionsType } from "@fumix/fu-blog-common";
import type { ConfirmDialogData, PublicPost, UserRolePermissionsType } from "@fumix/fu-blog-common";
import { asSearchOperator } from "@fumix/fu-blog-common";
import type { PropType } from "vue";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
Expand All @@ -87,10 +89,10 @@ const route = useRoute();
const router = useRouter();
const itemsPerPage = 12;
const loading = ref(true);
const posts = ref<Post[]>([]);
const posts = ref<PublicPost[]>([]);
const showDialog = ref<boolean>(false);
const dialogData = ref<ConfirmDialogData | null>(null);
const currentPost = ref<Post | null>(null);
const currentPost = ref<PublicPost | null>(null);
const totalPages = ref<number>(1);
const { t } = useI18n();
Expand All @@ -104,15 +106,11 @@ const props = defineProps({
},
});
const loadPostsWithPagination = async (pageIndex: number, search: string, operator: string) => {
const loadPostsWithPagination = async (pageIndex: number, search: string, operator: "and" | "or") => {
try {
let link = !search
? `/api/posts/page/${pageIndex}/count/${itemsPerPage}`
: `/api/posts/page/${pageIndex}/count/${itemsPerPage}/search/${encodeURIComponent(search)}/operator/${encodeURIComponent(operator)}`;
const res = await fetch(link);
const response = await res.json();
posts.value = response.data[0];
totalPages.value = Math.ceil((await response.data[1]) / itemsPerPage);
const [postResult, count] = await PostEndpoints.findPosts(pageIndex, 12, search, operator).then((it) => it.data);
posts.value = postResult;
totalPages.value = Math.ceil((count ?? 0) / itemsPerPage);
loading.value = false;
} catch (e) {
console.log("ERROR: ", e);
Expand All @@ -122,35 +120,35 @@ const loadPostsWithPagination = async (pageIndex: number, search: string, operat

const onPaginate = (page: number) => {
const searchValue = (route.query?.search || "") as string;
const operator = (route.query?.operator || "and") as string;
const operator = asSearchOperator(route.query?.operator?.toString());
loadPostsWithPagination(page, searchValue, operator);
};

watch(
() => route.query,
(query) => {
const searchValue = (query?.search || "") as string;
const operator = (route.query?.operator || "and") as string;
const searchValue = (query?.search ?? "") as string;
const operator = asSearchOperator(route.query?.operator?.toString());
loadPostsWithPagination(1, searchValue, operator);
},
);

onMounted(() => {
const searchValue = (route.query?.search || "") as string;
const operator = (route.query?.operator || "and") as string;
const operator = asSearchOperator(route.query?.operator?.toString());

blogTitle.value = "fumiX Blog";
blogShortDescription.value = "Alle Beiträge auf einen Blick";

loadPostsWithPagination(1, searchValue, operator);
});

const deletePost = async (post: Post) => {
const deletePost = async (post: PublicPost) => {
try {
const res = await fetch(`/api/posts/delete/${post.id}`);
await res.json();
const searchValue = (route.query?.search || "") as string;
const operator = (route.query?.operator || "and") as string;
const operator = asSearchOperator(route.query?.operator?.toString());
await loadPostsWithPagination(1, searchValue, operator);
} catch (e) {
console.log("ERROR: ", e);
Expand All @@ -165,8 +163,8 @@ const searchWord = (event: any) => {
goTo(`/posts/?search=${event[0]}&operator=and`);
};

const showConfirm = (post: Post) => {
currentPost.value = post as Post;
const showConfirm = (post: PublicPost) => {
currentPost.value = post as PublicPost;
dialogData.value = {
title: t("posts.confirm.title"),
message: t("posts.confirm.message", { post: currentPost.value.title }),
Expand All @@ -179,12 +177,12 @@ const canceled = () => {
};

const confirmed = () => {
deletePost(currentPost.value as Post);
deletePost(currentPost.value as PublicPost);
currentPost.value = null;
showDialog.value = false;
};

const changePost = (post: Post) => {
const changePost = (post: PublicPost) => {
goTo(`/posts/post/${post.id}/edit`);
};
</script>
2 changes: 2 additions & 0 deletions common/src/entity/UserRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const UserRoles = {
),
} as const;

export const DEFAULT_ROLE = new UserRolePermissions("Permissions of the logged out user (basically no permissions at all)", {});

export function permissionsForUser(user: User): UserRolePermissionsType {
return mergePermissions(user.roles.map((it) => UserRoles[it]));
}
Expand Down
Loading

0 comments on commit ce33119

Please sign in to comment.