From 3fd610aceaa500c9e5443b82968ee85950fef154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D0=B0=D1=82=D1=83=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Thu, 21 Aug 2025 19:07:24 +0300 Subject: [PATCH 1/4] achievements mvp --- package.json | 2 +- pnpm-lock.yaml | 30 ++-- src/App.vue | 35 ++-- .../achievements/AchievementCreateForm.vue | 34 ++++ .../achievements/AchievementDeleteButton.vue | 39 ++++ .../achievements/AchievementEditForm.vue | 32 ++++ .../achievements/AchievementGiveForm.vue | 50 ++++++ .../achievements/AchievementPictureForm.vue | 25 +++ .../AchievementReceiversTable.vue | 41 +++++ .../achievements/AchievementTable.vue | 40 +++++ .../achievements/UserAchievementsTable.vue | 48 +++++ src/models/achievements.ts | 11 ++ src/models/index.ts | 2 + src/models/toast.ts | 0 src/pages/Debug.vue | 9 + src/pages/MainPage.vue | 56 ------ src/pages/StartScreen.vue | 46 +++++ src/pages/achievements/AchievementCreate.vue | 10 ++ src/pages/achievements/AchievementGive.vue | 9 + src/pages/achievements/AchievementView.vue | 46 +++++ src/pages/achievements/AchievementsList.vue | 22 +++ src/pages/achievements/UserAchievements.vue | 17 ++ src/router/index.ts | 26 ++- src/store/achievementStore.ts | 166 ++++++++++++++++++ src/store/index.ts | 2 + src/store/profileStore.ts | 76 +++++++- src/store/toastStore.ts | 39 ++++ src/vuetify.ts | 5 + tsconfig.json | 4 +- 29 files changed, 818 insertions(+), 104 deletions(-) create mode 100644 src/components/achievements/AchievementCreateForm.vue create mode 100644 src/components/achievements/AchievementDeleteButton.vue create mode 100644 src/components/achievements/AchievementEditForm.vue create mode 100644 src/components/achievements/AchievementGiveForm.vue create mode 100644 src/components/achievements/AchievementPictureForm.vue create mode 100644 src/components/achievements/AchievementReceiversTable.vue create mode 100644 src/components/achievements/AchievementTable.vue create mode 100644 src/components/achievements/UserAchievementsTable.vue create mode 100644 src/models/achievements.ts create mode 100644 src/models/index.ts create mode 100644 src/models/toast.ts create mode 100644 src/pages/Debug.vue delete mode 100644 src/pages/MainPage.vue create mode 100644 src/pages/StartScreen.vue create mode 100644 src/pages/achievements/AchievementCreate.vue create mode 100644 src/pages/achievements/AchievementGive.vue create mode 100644 src/pages/achievements/AchievementView.vue create mode 100644 src/pages/achievements/AchievementsList.vue create mode 100644 src/pages/achievements/UserAchievements.vue create mode 100644 src/store/achievementStore.ts create mode 100644 src/store/toastStore.ts diff --git a/package.json b/package.json index 5b4a789..7029b5d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,6 @@ "vite-plugin-vuetify": "^2.1.0", "vite-svg-loader": "^5.1.0", "vue-tsc": "^2.2.6", - "vuetify": "^3.7.14" + "vuetify": "^3.9.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9625b8d..e357e79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,7 +107,7 @@ importers: version: 6.0.0(postcss@8.5.3)(rollup@4.34.9)(stylelint@16.15.0(typescript@5.8.2))(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)) vite-plugin-vuetify: specifier: ^2.1.0 - version: 2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.7.14) + version: 2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.9.5) vite-svg-loader: specifier: ^5.1.0 version: 5.1.0(vue@3.5.13(typescript@5.8.2)) @@ -115,8 +115,8 @@ importers: specifier: ^2.2.6 version: 2.2.6(typescript@5.8.2) vuetify: - specifier: ^3.7.14 - version: 3.7.14(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) + specifier: ^3.9.5 + version: 3.9.5(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) packages: @@ -2384,14 +2384,14 @@ packages: typescript: optional: true - vuetify@3.7.14: - resolution: {integrity: sha512-L5oD0x3Wrp49Khp+16dRykAFUZq6WXqX7OHLTwXnyEliJ48ERC9WfEtN0YOuEIhor/b2IOZXl+11fs/m452/Lw==} + vuetify@3.9.5: + resolution: {integrity: sha512-rJBSo1FeKcJJaqTfVHbOdpFXCmgeEIGzrh6HBEMGjSflan6voPIMSiK2dTCUU4t9JeghwvJtoAE5eBuqYIkFVA==} engines: {node: ^12.20 || >=14.13} peerDependencies: typescript: '>=4.7' - vite-plugin-vuetify: '>=1.0.0' - vue: ^3.3.0 - webpack-plugin-vuetify: '>=2.0.0' + vite-plugin-vuetify: '>=2.1.0' + vue: ^3.5.0 + webpack-plugin-vuetify: '>=3.1.0' peerDependenciesMeta: typescript: optional: true @@ -3223,11 +3223,11 @@ snapshots: '@vue/shared@3.5.13': {} - '@vuetify/loader-shared@2.1.0(vue@3.5.13(typescript@5.8.2))(vuetify@3.7.14)': + '@vuetify/loader-shared@2.1.0(vue@3.5.13(typescript@5.8.2))(vuetify@3.9.5)': dependencies: upath: 2.0.1 vue: 3.5.13(typescript@5.8.2) - vuetify: 3.7.14(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) + vuetify: 3.9.5(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) acorn-jsx@5.3.2(acorn@8.14.0): dependencies: @@ -4680,14 +4680,14 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vuetify@2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.7.14): + vite-plugin-vuetify@2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.9.5): dependencies: - '@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.2))(vuetify@3.7.14) + '@vuetify/loader-shared': 2.1.0(vue@3.5.13(typescript@5.8.2))(vuetify@3.9.5) debug: 4.4.0(supports-color@9.4.0) upath: 2.0.1 vite: 6.2.0(@types/node@22.13.8)(jiti@2.4.2) vue: 3.5.13(typescript@5.8.2) - vuetify: 3.7.14(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) + vuetify: 3.9.5(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)) transitivePeerDependencies: - supports-color @@ -4742,12 +4742,12 @@ snapshots: optionalDependencies: typescript: 5.8.2 - vuetify@3.7.14(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)): + vuetify@3.9.5(typescript@5.8.2)(vite-plugin-vuetify@2.1.0)(vue@3.5.13(typescript@5.8.2)): dependencies: vue: 3.5.13(typescript@5.8.2) optionalDependencies: typescript: 5.8.2 - vite-plugin-vuetify: 2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.7.14) + vite-plugin-vuetify: 2.1.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2))(vue@3.5.13(typescript@5.8.2))(vuetify@3.9.5) wcwidth@1.0.1: dependencies: diff --git a/src/App.vue b/src/App.vue index 402c126..e9d7466 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,24 +1,24 @@ diff --git a/src/components/achievements/AchievementCreateForm.vue b/src/components/achievements/AchievementCreateForm.vue new file mode 100644 index 0000000..384f165 --- /dev/null +++ b/src/components/achievements/AchievementCreateForm.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/src/components/achievements/AchievementDeleteButton.vue b/src/components/achievements/AchievementDeleteButton.vue new file mode 100644 index 0000000..2b5d8b1 --- /dev/null +++ b/src/components/achievements/AchievementDeleteButton.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/components/achievements/AchievementEditForm.vue b/src/components/achievements/AchievementEditForm.vue new file mode 100644 index 0000000..04aca7e --- /dev/null +++ b/src/components/achievements/AchievementEditForm.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/components/achievements/AchievementGiveForm.vue b/src/components/achievements/AchievementGiveForm.vue new file mode 100644 index 0000000..784ee01 --- /dev/null +++ b/src/components/achievements/AchievementGiveForm.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/components/achievements/AchievementPictureForm.vue b/src/components/achievements/AchievementPictureForm.vue new file mode 100644 index 0000000..0be08e1 --- /dev/null +++ b/src/components/achievements/AchievementPictureForm.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/components/achievements/AchievementReceiversTable.vue b/src/components/achievements/AchievementReceiversTable.vue new file mode 100644 index 0000000..e3f8b02 --- /dev/null +++ b/src/components/achievements/AchievementReceiversTable.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/achievements/AchievementTable.vue b/src/components/achievements/AchievementTable.vue new file mode 100644 index 0000000..3861f12 --- /dev/null +++ b/src/components/achievements/AchievementTable.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/src/components/achievements/UserAchievementsTable.vue b/src/components/achievements/UserAchievementsTable.vue new file mode 100644 index 0000000..8e2bfd8 --- /dev/null +++ b/src/components/achievements/UserAchievementsTable.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/models/achievements.ts b/src/models/achievements.ts new file mode 100644 index 0000000..fb8bf2a --- /dev/null +++ b/src/models/achievements.ts @@ -0,0 +1,11 @@ +export interface Achievement { + id: number, + name: string, + description: string, + picture: string | null, + owner_user_id: number, +} + +export interface Receiver { + user_id: number, +} \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..0024d7c --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,2 @@ +export * from "./achievements" + diff --git a/src/models/toast.ts b/src/models/toast.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Debug.vue b/src/pages/Debug.vue new file mode 100644 index 0000000..d2e5f15 --- /dev/null +++ b/src/pages/Debug.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/pages/MainPage.vue b/src/pages/MainPage.vue deleted file mode 100644 index cc109f1..0000000 --- a/src/pages/MainPage.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/src/pages/StartScreen.vue b/src/pages/StartScreen.vue new file mode 100644 index 0000000..8305620 --- /dev/null +++ b/src/pages/StartScreen.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/pages/achievements/AchievementCreate.vue b/src/pages/achievements/AchievementCreate.vue new file mode 100644 index 0000000..077ddf8 --- /dev/null +++ b/src/pages/achievements/AchievementCreate.vue @@ -0,0 +1,10 @@ + + + + + diff --git a/src/pages/achievements/AchievementGive.vue b/src/pages/achievements/AchievementGive.vue new file mode 100644 index 0000000..b2fb0f9 --- /dev/null +++ b/src/pages/achievements/AchievementGive.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/pages/achievements/AchievementView.vue b/src/pages/achievements/AchievementView.vue new file mode 100644 index 0000000..d849131 --- /dev/null +++ b/src/pages/achievements/AchievementView.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/pages/achievements/AchievementsList.vue b/src/pages/achievements/AchievementsList.vue new file mode 100644 index 0000000..e4253d2 --- /dev/null +++ b/src/pages/achievements/AchievementsList.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/pages/achievements/UserAchievements.vue b/src/pages/achievements/UserAchievements.vue new file mode 100644 index 0000000..9d6d2aa --- /dev/null +++ b/src/pages/achievements/UserAchievements.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index ca3a585..d4764a3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -3,8 +3,32 @@ import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'; const routes: RouteRecordRaw[] = [ { path: '/', - component: () => import('../pages/MainPage.vue'), + component: () => import('../pages/StartScreen.vue'), }, + { + path: '/achievements', + component: () => import('../pages/achievements/AchievementsList.vue'), + }, + { + path: '/achievements/:id', + component: () => import('../pages/achievements/AchievementView.vue'), + }, + { + path: '/achievements/create', + component: () => import('../pages/achievements/AchievementCreate.vue'), + }, + { + path: '/achievements/give', + component: () => import('../pages/achievements/AchievementGive.vue'), + }, + { + path: '/achievements/user/:id', + component: () => import('../pages/achievements/UserAchievements.vue'), + }, + { + path: '/debug', + component: () => import('../pages/Debug.vue'), + } ]; export const router = createRouter({ diff --git a/src/store/achievementStore.ts b/src/store/achievementStore.ts new file mode 100644 index 0000000..ed3c394 --- /dev/null +++ b/src/store/achievementStore.ts @@ -0,0 +1,166 @@ +import apiClient from "@/api"; +import { Achievement, Receiver } from "@/models/achievements"; +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { useToastStore } from "./toastStore"; + +export const useAchievementStore = defineStore("achievement", () => { + const toastStore = useToastStore(); + + const defaultAchievement: Achievement = { + id: 0, + name: "Дефолтная ачивка", + description: 'Когда ничего другого не загрузилось', + picture: null, + owner_user_id: 177, + } + + const allAchievements = ref([]); + const achievement = ref(defaultAchievement); + const receivers = ref([]); + const userAchivements = ref([]); + + async function requestAllAchievements() { + const { data, error } = await apiClient.GET('/achievement/achievement'); + if (error) { + toastStore.error(error); + return; + } + allAchievements.value = data.sort((a, b) => a.id - b.id); + } + + async function requestAchievement(id: number) { + const { data, error } = await apiClient.GET('/achievement/achievement/{id}', { + params: { path: { id } } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + achievement.value = data; + } + + async function create(name: string, description: string, picture_file: File | null) { + const { data, error } = await apiClient.POST("/achievement/achievement", { + body: { name, description } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + if (!picture_file) { + toastStore.success(`Достижение ${data.id} создано! Фотографию можно дозагрузить на странице достижения`); + return data; + } + toastStore.success(`Достижение ${data.id} создано! Загружаем иконку...`); + return changePicture(data.id, picture_file); + } + + async function edit(id: number, name: string, description: string) { + const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}', { + params: { path: { id } }, + body: { name, description } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + achievement.value = data; + toastStore.success(`Достижение ${id} изменено. Иконка меняется отдельно`); + } + + async function changePicture(id: number, picture_file: File) { + const formData = new FormData(); + formData.append('picture_file', picture_file); + const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}/picture', { + params: { path: { id } }, + body: formData as never, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Фотография достижения ${id} изменена`); + achievement.value = data; + return data; + } + + async function deleteAchievement(id: number) { + const { error } = await apiClient.DELETE('/achievement/achievement/{id}', { + params: { path: { id } } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Достижение ${id} удалено`); + } + + async function requestReceivers(achievement_id: number) { + const { data, error } = await apiClient.GET("/achievement/achievement/{achievement_id}/reciever", { + params: { path: { achievement_id } } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + receivers.value = data.recievers; + } + + async function createReceiver(achievement_id: number, user_id: number) { + const { data, error } = await apiClient.POST('/achievement/achievement/{achievement_id}/reciever/{user_id}', { + params: { path: { achievement_id, user_id } } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Ачивка ${achievement_id} выдана пользователю ${user_id}`); + receivers.value = data.recievers; + } + + async function revokeReceiver(achievement_id: number, user_id: number) { + const { data, error } = await apiClient.DELETE('/achievement/achievement/{achievement_id}/reciever/{user_id}', { + params: { path: { achievement_id, user_id } } + }) + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Ачивка ${achievement_id} забрана у пользователю ${user_id}`); + receivers.value = data.recievers; + await requestUserAchievements(user_id); + } + + async function requestUserAchievements(user_id: number) { + const { data, error } = await apiClient.GET('/achievement/user/{user_id}', { + params: { path: { user_id } } + }) + if (error) { + toastStore.error(error.detail![0].msg ?? "Неизвестная ошибка"); + return; + } + userAchivements.value = data.achievement.sort((a, b) => a.id - b.id); + } + + return { + allAchievements, + achievement, + receivers, + userAchivements, + + requestAllAchievements, + + requestAchievement, + create, + edit, + changePicture, + deleteAchievement, + + requestReceivers, + createReceiver, + revokeReceiver, + + requestUserAchievements, + } +}) \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 21feb3f..18a68b5 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1 +1,3 @@ +export { useAchievementStore } from './achievementStore'; export { useProfileStore } from './profileStore'; +export { useToastStore } from './toastStore' diff --git a/src/store/profileStore.ts b/src/store/profileStore.ts index 28d0c8a..181ddef 100644 --- a/src/store/profileStore.ts +++ b/src/store/profileStore.ts @@ -1,8 +1,13 @@ import { defineStore } from 'pinia'; -import { ref } from 'vue'; +import { computed, ref } from 'vue'; +import { setupAuth } from '@profcomff/api-uilib'; +import apiClient from '@/api'; +import { useToastStore } from './toastStore'; export const useProfileStore = defineStore('profile', () => { - const id = ref(null); + const toastStore = useToastStore(); + + const user_id = ref(null); const email = ref(null); const token = ref(null); const groups = ref(null); @@ -21,15 +26,72 @@ export const useProfileStore = defineStore('profile', () => { token.value = currToken; } if (currId) { - id.value = +currId; + user_id.value = +currId; } if (currScopes) { sessionScopes.value = currScopes.split(','); } }; + // if debugging, paste it here to avoid relogging + const TVOI_FF_TEST_TOKEN = ''; + + async function setupAdminSession(tvff_token: string | null) { + const storedToken = localStorage.getItem('token'); + if (storedToken) { + const { data } = await apiClient.GET('/auth/me'); + if (data) { + setupAuth(storedToken); + toastStore.success("Логин из сохраненной сессии успешен"); + return; + } + toastStore.error("Сохраненная сессия истекла, залогиньтесь заново"); + localStorage.removeItem("token"); + } + + setupAuth(tvff_token ?? TVOI_FF_TEST_TOKEN); + + const serviceScopes = [ + 'achievement.edit', + 'achievement.give', + 'achievement.revoke', + 'achievement.delete', + 'achievement.create', + ]; + const serviceName = 'achievements'; + const scopes = serviceScopes.map(value => `${serviceName}.${value}`); + const { data, error } = await apiClient.POST('/auth/session', { + body: { + scopes, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }, + }); + + if (error) { + toastStore.error(error.detail as unknown as string ?? "Что-то не так"); + return; + } + + if (data) { + user_id.value = data.id; + token.value = data.token || ''; + sessionScopes.value = data.session_scopes ?? []; + + setupAuth(data.token || ''); + localStorage.setItem("token", data.token || ''); + toastStore.success("Сессия установлена и сохранена"); + } + } + + async function setupUserSession(tvff_token: string | null) { + setupAuth(tvff_token ?? TVOI_FF_TEST_TOKEN); + toastStore.success('user logged'); + } + + const isLogged = computed(() => token.value && token.value !== ''); + return { - id, + user_id, email, token, groups, @@ -39,6 +101,10 @@ export const useProfileStore = defineStore('profile', () => { full_name, + isLogged, + fromUrl, + setupAdminSession, + setupUserSession, }; -}); +}); \ No newline at end of file diff --git a/src/store/toastStore.ts b/src/store/toastStore.ts new file mode 100644 index 0000000..2c1ff9f --- /dev/null +++ b/src/store/toastStore.ts @@ -0,0 +1,39 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; + +export const useToastStore = defineStore("toast", () => { + interface Toast { + text: string, + timeout?: number, + color?: string, + } + + const queue = ref([]); + + function add(message: Toast) { + queue.value.push(message); + } + + function success(text: string) { + queue.value.push({ + text, + timeout: 2000, + color: "success", + }) + } + + function error(text: string) { + queue.value.push({ + text, + timeout: 2000, + color: "error", + }) + } + + return { + queue, + add, + error, + success + } +}) \ No newline at end of file diff --git a/src/vuetify.ts b/src/vuetify.ts index 6a6c506..7cc5ea5 100644 --- a/src/vuetify.ts +++ b/src/vuetify.ts @@ -1,4 +1,5 @@ import '@mdi/font/css/materialdesignicons.css'; +import 'vuetify/styles' import { createVuetify } from 'vuetify'; import { aliases, mdi } from 'vuetify/iconsets/mdi'; @@ -31,6 +32,10 @@ export const vuetify = createVuetify({ }, }, defaults: { + VBtn: { + variant: 'flat', + color: 'primary', + }, VBtnToggle: { color: 'rgb(0, 1, 76)', elevation: 1, diff --git a/tsconfig.json b/tsconfig.json index 0cc7fa6..070084c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,14 +9,14 @@ "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, - "lib": ["ESNext", "DOM"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "skipLibCheck": true, "noEmit": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] - }, + } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [ From 4d923498567cc6d5add3a95f01f9da8dadd73dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D0=B0=D1=82=D1=83=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Thu, 21 Aug 2025 19:08:53 +0300 Subject: [PATCH 2/4] linting --- src/models/achievements.ts | 14 +- src/models/index.ts | 3 +- src/pages/{Debug.vue => DebugPage.vue} | 0 src/router/index.ts | 4 +- src/store/achievementStore.ts | 332 ++++++++++++------------- src/store/index.ts | 2 +- src/store/profileStore.ts | 14 +- src/store/toastStore.ts | 66 ++--- src/vuetify.ts | 2 +- 9 files changed, 218 insertions(+), 219 deletions(-) rename src/pages/{Debug.vue => DebugPage.vue} (100%) diff --git a/src/models/achievements.ts b/src/models/achievements.ts index fb8bf2a..4ac6fa3 100644 --- a/src/models/achievements.ts +++ b/src/models/achievements.ts @@ -1,11 +1,11 @@ export interface Achievement { - id: number, - name: string, - description: string, - picture: string | null, - owner_user_id: number, + id: number; + name: string; + description: string; + picture: string | null; + owner_user_id: number; } export interface Receiver { - user_id: number, -} \ No newline at end of file + user_id: number; +} diff --git a/src/models/index.ts b/src/models/index.ts index 0024d7c..1995b1c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,2 +1 @@ -export * from "./achievements" - +export * from './achievements'; diff --git a/src/pages/Debug.vue b/src/pages/DebugPage.vue similarity index 100% rename from src/pages/Debug.vue rename to src/pages/DebugPage.vue diff --git a/src/router/index.ts b/src/router/index.ts index d4764a3..42882f1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -27,8 +27,8 @@ const routes: RouteRecordRaw[] = [ }, { path: '/debug', - component: () => import('../pages/Debug.vue'), - } + component: () => import('../pages/DebugPage.vue'), + }, ]; export const router = createRouter({ diff --git a/src/store/achievementStore.ts b/src/store/achievementStore.ts index ed3c394..91ba461 100644 --- a/src/store/achievementStore.ts +++ b/src/store/achievementStore.ts @@ -1,166 +1,166 @@ -import apiClient from "@/api"; -import { Achievement, Receiver } from "@/models/achievements"; -import { defineStore } from "pinia"; -import { ref } from "vue"; -import { useToastStore } from "./toastStore"; - -export const useAchievementStore = defineStore("achievement", () => { - const toastStore = useToastStore(); - - const defaultAchievement: Achievement = { - id: 0, - name: "Дефолтная ачивка", - description: 'Когда ничего другого не загрузилось', - picture: null, - owner_user_id: 177, - } - - const allAchievements = ref([]); - const achievement = ref(defaultAchievement); - const receivers = ref([]); - const userAchivements = ref([]); - - async function requestAllAchievements() { - const { data, error } = await apiClient.GET('/achievement/achievement'); - if (error) { - toastStore.error(error); - return; - } - allAchievements.value = data.sort((a, b) => a.id - b.id); - } - - async function requestAchievement(id: number) { - const { data, error } = await apiClient.GET('/achievement/achievement/{id}', { - params: { path: { id } } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - achievement.value = data; - } - - async function create(name: string, description: string, picture_file: File | null) { - const { data, error } = await apiClient.POST("/achievement/achievement", { - body: { name, description } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - if (!picture_file) { - toastStore.success(`Достижение ${data.id} создано! Фотографию можно дозагрузить на странице достижения`); - return data; - } - toastStore.success(`Достижение ${data.id} создано! Загружаем иконку...`); - return changePicture(data.id, picture_file); - } - - async function edit(id: number, name: string, description: string) { - const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}', { - params: { path: { id } }, - body: { name, description } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - achievement.value = data; - toastStore.success(`Достижение ${id} изменено. Иконка меняется отдельно`); - } - - async function changePicture(id: number, picture_file: File) { - const formData = new FormData(); - formData.append('picture_file', picture_file); - const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}/picture', { - params: { path: { id } }, - body: formData as never, - }); - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - toastStore.success(`Фотография достижения ${id} изменена`); - achievement.value = data; - return data; - } - - async function deleteAchievement(id: number) { - const { error } = await apiClient.DELETE('/achievement/achievement/{id}', { - params: { path: { id } } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - toastStore.success(`Достижение ${id} удалено`); - } - - async function requestReceivers(achievement_id: number) { - const { data, error } = await apiClient.GET("/achievement/achievement/{achievement_id}/reciever", { - params: { path: { achievement_id } } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - receivers.value = data.recievers; - } - - async function createReceiver(achievement_id: number, user_id: number) { - const { data, error } = await apiClient.POST('/achievement/achievement/{achievement_id}/reciever/{user_id}', { - params: { path: { achievement_id, user_id } } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - toastStore.success(`Ачивка ${achievement_id} выдана пользователю ${user_id}`); - receivers.value = data.recievers; - } - - async function revokeReceiver(achievement_id: number, user_id: number) { - const { data, error } = await apiClient.DELETE('/achievement/achievement/{achievement_id}/reciever/{user_id}', { - params: { path: { achievement_id, user_id } } - }) - if (error) { - toastStore.error(error.detail![0].msg); - return; - } - toastStore.success(`Ачивка ${achievement_id} забрана у пользователю ${user_id}`); - receivers.value = data.recievers; - await requestUserAchievements(user_id); - } - - async function requestUserAchievements(user_id: number) { - const { data, error } = await apiClient.GET('/achievement/user/{user_id}', { - params: { path: { user_id } } - }) - if (error) { - toastStore.error(error.detail![0].msg ?? "Неизвестная ошибка"); - return; - } - userAchivements.value = data.achievement.sort((a, b) => a.id - b.id); - } - - return { - allAchievements, - achievement, - receivers, - userAchivements, - - requestAllAchievements, - - requestAchievement, - create, - edit, - changePicture, - deleteAchievement, - - requestReceivers, - createReceiver, - revokeReceiver, - - requestUserAchievements, - } -}) \ No newline at end of file +import apiClient from '@/api'; +import { Achievement, Receiver } from '@/models/achievements'; +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { useToastStore } from './toastStore'; + +export const useAchievementStore = defineStore('achievement', () => { + const toastStore = useToastStore(); + + const defaultAchievement: Achievement = { + id: 0, + name: 'Дефолтная ачивка', + description: 'Когда ничего другого не загрузилось', + picture: null, + owner_user_id: 177, + }; + + const allAchievements = ref([]); + const achievement = ref(defaultAchievement); + const receivers = ref([]); + const userAchivements = ref([]); + + async function requestAllAchievements() { + const { data, error } = await apiClient.GET('/achievement/achievement'); + if (error) { + toastStore.error(error); + return; + } + allAchievements.value = data.sort((a, b) => a.id - b.id); + } + + async function requestAchievement(id: number) { + const { data, error } = await apiClient.GET('/achievement/achievement/{id}', { + params: { path: { id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + achievement.value = data; + } + + async function create(name: string, description: string, picture_file: File | null) { + const { data, error } = await apiClient.POST('/achievement/achievement', { + body: { name, description }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + if (!picture_file) { + toastStore.success(`Достижение ${data.id} создано! Фотографию можно дозагрузить на странице достижения`); + return data; + } + toastStore.success(`Достижение ${data.id} создано! Загружаем иконку...`); + return changePicture(data.id, picture_file); + } + + async function edit(id: number, name: string, description: string) { + const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}', { + params: { path: { id } }, + body: { name, description }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + achievement.value = data; + toastStore.success(`Достижение ${id} изменено. Иконка меняется отдельно`); + } + + async function changePicture(id: number, picture_file: File) { + const formData = new FormData(); + formData.append('picture_file', picture_file); + const { data, error } = await apiClient.PATCH('/achievement/achievement/{id}/picture', { + params: { path: { id } }, + body: formData as never, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Фотография достижения ${id} изменена`); + achievement.value = data; + return data; + } + + async function deleteAchievement(id: number) { + const { error } = await apiClient.DELETE('/achievement/achievement/{id}', { + params: { path: { id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Достижение ${id} удалено`); + } + + async function requestReceivers(achievement_id: number) { + const { data, error } = await apiClient.GET('/achievement/achievement/{achievement_id}/reciever', { + params: { path: { achievement_id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + receivers.value = data.recievers; + } + + async function createReceiver(achievement_id: number, user_id: number) { + const { data, error } = await apiClient.POST('/achievement/achievement/{achievement_id}/reciever/{user_id}', { + params: { path: { achievement_id, user_id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Ачивка ${achievement_id} выдана пользователю ${user_id}`); + receivers.value = data.recievers; + } + + async function revokeReceiver(achievement_id: number, user_id: number) { + const { data, error } = await apiClient.DELETE('/achievement/achievement/{achievement_id}/reciever/{user_id}', { + params: { path: { achievement_id, user_id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg); + return; + } + toastStore.success(`Ачивка ${achievement_id} забрана у пользователю ${user_id}`); + receivers.value = data.recievers; + await requestUserAchievements(user_id); + } + + async function requestUserAchievements(user_id: number) { + const { data, error } = await apiClient.GET('/achievement/user/{user_id}', { + params: { path: { user_id } }, + }); + if (error) { + toastStore.error(error.detail![0].msg ?? 'Неизвестная ошибка'); + return; + } + userAchivements.value = data.achievement.sort((a, b) => a.id - b.id); + } + + return { + allAchievements, + achievement, + receivers, + userAchivements, + + requestAllAchievements, + + requestAchievement, + create, + edit, + changePicture, + deleteAchievement, + + requestReceivers, + createReceiver, + revokeReceiver, + + requestUserAchievements, + }; +}); diff --git a/src/store/index.ts b/src/store/index.ts index 18a68b5..765d627 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,3 +1,3 @@ export { useAchievementStore } from './achievementStore'; export { useProfileStore } from './profileStore'; -export { useToastStore } from './toastStore' +export { useToastStore } from './toastStore'; diff --git a/src/store/profileStore.ts b/src/store/profileStore.ts index 181ddef..03f886e 100644 --- a/src/store/profileStore.ts +++ b/src/store/profileStore.ts @@ -42,11 +42,11 @@ export const useProfileStore = defineStore('profile', () => { const { data } = await apiClient.GET('/auth/me'); if (data) { setupAuth(storedToken); - toastStore.success("Логин из сохраненной сессии успешен"); + toastStore.success('Логин из сохраненной сессии успешен'); return; } - toastStore.error("Сохраненная сессия истекла, залогиньтесь заново"); - localStorage.removeItem("token"); + toastStore.error('Сохраненная сессия истекла, залогиньтесь заново'); + localStorage.removeItem('token'); } setupAuth(tvff_token ?? TVOI_FF_TEST_TOKEN); @@ -68,7 +68,7 @@ export const useProfileStore = defineStore('profile', () => { }); if (error) { - toastStore.error(error.detail as unknown as string ?? "Что-то не так"); + toastStore.error((error.detail as unknown as string) ?? 'Что-то не так'); return; } @@ -78,8 +78,8 @@ export const useProfileStore = defineStore('profile', () => { sessionScopes.value = data.session_scopes ?? []; setupAuth(data.token || ''); - localStorage.setItem("token", data.token || ''); - toastStore.success("Сессия установлена и сохранена"); + localStorage.setItem('token', data.token || ''); + toastStore.success('Сессия установлена и сохранена'); } } @@ -107,4 +107,4 @@ export const useProfileStore = defineStore('profile', () => { setupAdminSession, setupUserSession, }; -}); \ No newline at end of file +}); diff --git a/src/store/toastStore.ts b/src/store/toastStore.ts index 2c1ff9f..1125a31 100644 --- a/src/store/toastStore.ts +++ b/src/store/toastStore.ts @@ -1,39 +1,39 @@ -import { defineStore } from "pinia"; -import { ref } from "vue"; +import { defineStore } from 'pinia'; +import { ref } from 'vue'; -export const useToastStore = defineStore("toast", () => { - interface Toast { - text: string, - timeout?: number, - color?: string, - } +export const useToastStore = defineStore('toast', () => { + interface Toast { + text: string; + timeout?: number; + color?: string; + } - const queue = ref([]); + const queue = ref([]); - function add(message: Toast) { - queue.value.push(message); - } + function add(message: Toast) { + queue.value.push(message); + } - function success(text: string) { - queue.value.push({ - text, - timeout: 2000, - color: "success", - }) - } + function success(text: string) { + queue.value.push({ + text, + timeout: 2000, + color: 'success', + }); + } - function error(text: string) { - queue.value.push({ - text, - timeout: 2000, - color: "error", - }) - } + function error(text: string) { + queue.value.push({ + text, + timeout: 2000, + color: 'error', + }); + } - return { - queue, - add, - error, - success - } -}) \ No newline at end of file + return { + queue, + add, + error, + success, + }; +}); diff --git a/src/vuetify.ts b/src/vuetify.ts index 7cc5ea5..a825683 100644 --- a/src/vuetify.ts +++ b/src/vuetify.ts @@ -1,5 +1,5 @@ import '@mdi/font/css/materialdesignicons.css'; -import 'vuetify/styles' +import 'vuetify/styles'; import { createVuetify } from 'vuetify'; import { aliases, mdi } from 'vuetify/iconsets/mdi'; From 18a55f1b3a2a14d67313060d658fd103874dbe7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D0=B0=D1=82=D1=83=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Thu, 21 Aug 2025 19:09:33 +0300 Subject: [PATCH 3/4] linting --- src/models/index.ts | 1 - src/models/toast.ts | 0 2 files changed, 1 deletion(-) delete mode 100644 src/models/index.ts delete mode 100644 src/models/toast.ts diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index 1995b1c..0000000 --- a/src/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './achievements'; diff --git a/src/models/toast.ts b/src/models/toast.ts deleted file mode 100644 index e69de29..0000000 From a2c357a479c7159b2db1aef36132447675b6553c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D0=B0=D1=82=D1=83=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Thu, 21 Aug 2025 19:27:29 +0300 Subject: [PATCH 4/4] postcss bullshit --- package.json | 1 + pnpm-lock.yaml | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7029b5d..a73828f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint-plugin-vue": "^9.32.0", "knip": "^5.45.0", "openapi-typescript": "^7.6.1", + "postcss-html": "^1.8.0", "postcss-preset-env": "^10.1.5", "prettier": "^3.5.2", "stylelint": "^16.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e357e79..0637a84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: openapi-typescript: specifier: ^7.6.1 version: 7.6.1(typescript@5.8.2) + postcss-html: + specifier: ^1.8.0 + version: 1.8.0 postcss-preset-env: specifier: ^10.1.5 version: 10.1.5(postcss@8.5.3) @@ -86,7 +89,7 @@ importers: version: 15.0.0(stylelint@16.15.0(typescript@5.8.2)) stylelint-config-recommended-vue: specifier: ^1.6.0 - version: 1.6.0(postcss-html@1.7.0)(stylelint@16.15.0(typescript@5.8.2)) + version: 1.6.0(postcss-html@1.8.0)(stylelint@16.15.0(typescript@5.8.2)) stylelint-config-standard: specifier: ^37.0.0 version: 37.0.0(stylelint@16.15.0(typescript@5.8.2)) @@ -1898,8 +1901,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-html@1.7.0: - resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==} + postcss-html@1.8.0: + resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} engines: {node: ^12 || >=14} postcss-image-set-function@7.0.0: @@ -4209,7 +4212,7 @@ snapshots: dependencies: postcss: 8.5.3 - postcss-html@1.7.0: + postcss-html@1.8.0: dependencies: htmlparser2: 8.0.2 js-tokens: 9.0.1 @@ -4504,17 +4507,17 @@ snapshots: strip-json-comments@5.0.1: {} - stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.15.0(typescript@5.8.2)): + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.15.0(typescript@5.8.2)): dependencies: - postcss-html: 1.7.0 + postcss-html: 1.8.0 stylelint: 16.15.0(typescript@5.8.2) - stylelint-config-recommended-vue@1.6.0(postcss-html@1.7.0)(stylelint@16.15.0(typescript@5.8.2)): + stylelint-config-recommended-vue@1.6.0(postcss-html@1.8.0)(stylelint@16.15.0(typescript@5.8.2)): dependencies: - postcss-html: 1.7.0 + postcss-html: 1.8.0 semver: 7.7.1 stylelint: 16.15.0(typescript@5.8.2) - stylelint-config-html: 1.1.0(postcss-html@1.7.0)(stylelint@16.15.0(typescript@5.8.2)) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@16.15.0(typescript@5.8.2)) stylelint-config-recommended: 15.0.0(stylelint@16.15.0(typescript@5.8.2)) stylelint-config-recommended@15.0.0(stylelint@16.15.0(typescript@5.8.2)):