From 2a0c89be2ad74c58254dbb40f01a587bd4e6eff1 Mon Sep 17 00:00:00 2001
From: zamelane <zamelane@vk.com>
Date: Wed, 4 Dec 2024 01:38:43 +0700
Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D0=BE=D1=82=D0=BE=D0=B2?=
 =?UTF-8?q?=D0=BA=D0=B0=20=D0=BC=D0=B0=D0=B3=D0=B0=D0=B7=D0=B8=D0=BD=D0=B0?=
 =?UTF-8?q?=20=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B8=D0=B5=20=D1=84?=
 =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D1=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/api/.gitignore                           |   2 +
 apps/api/package.json                         |   3 +
 .../src/helpers/syncDatabaseForDecorations.ts | 108 ++++++++++
 apps/api/src/main.ts                          |   8 +
 apps/api/src/models/Avatar/index.ts           |   1 +
 apps/api/src/models/Avatar/model.ts           |  42 ++++
 apps/api/src/models/AvatarTag/index.ts        |   1 +
 apps/api/src/models/AvatarTag/model.ts        |  40 ++++
 apps/api/src/models/DiaryUser/model.ts        |   9 +
 apps/api/src/models/Tag/index.ts              |   1 +
 apps/api/src/models/Tag/model.ts              |  33 +++
 apps/api/src/models/UserAvatar/index.ts       |   1 +
 apps/api/src/models/UserAvatar/model.ts       |  38 ++++
 apps/api/src/models/relations.ts              |  23 ++-
 apps/api/src/routes/index.ts                  |   2 +
 apps/api/src/routes/marketAvatars/handler.ts  |  45 +++++
 apps/api/src/routes/marketAvatars/index.ts    |  17 ++
 apps/api/src/uploads/decorations.json         |  16 ++
 apps/shared/src/api/self/index.ts             |   7 +
 apps/web/.env.development                     |   3 +-
 .../ModalRoot/modals/UserEditModal/index.tsx  |  18 +-
 .../pages/Market/components/avatarsPanel.tsx  | 149 ++++++++------
 .../pages/Market/components/filtersPanel.tsx  | 190 +++++++++---------
 .../pages/Market/components/marketHeader.tsx  |   7 +-
 .../web/src/pages/Market/components/types.tsx |   6 -
 apps/web/src/pages/Market/index.tsx           | 138 ++++++-------
 apps/web/src/pages/Settings/Actions/index.tsx |  24 ++-
 apps/web/src/shared/config/constants.ts       |   2 +-
 .../recent-marks/ui/LessonGrades/index.tsx    |   1 +
 bun.lockb                                     | Bin 158072 -> 159968 bytes
 30 files changed, 675 insertions(+), 260 deletions(-)
 create mode 100644 apps/api/src/helpers/syncDatabaseForDecorations.ts
 create mode 100644 apps/api/src/models/Avatar/index.ts
 create mode 100644 apps/api/src/models/Avatar/model.ts
 create mode 100644 apps/api/src/models/AvatarTag/index.ts
 create mode 100644 apps/api/src/models/AvatarTag/model.ts
 create mode 100644 apps/api/src/models/Tag/index.ts
 create mode 100644 apps/api/src/models/Tag/model.ts
 create mode 100644 apps/api/src/models/UserAvatar/index.ts
 create mode 100644 apps/api/src/models/UserAvatar/model.ts
 create mode 100644 apps/api/src/routes/marketAvatars/handler.ts
 create mode 100644 apps/api/src/routes/marketAvatars/index.ts
 create mode 100644 apps/api/src/uploads/decorations.json
 delete mode 100644 apps/web/src/pages/Market/components/types.tsx

diff --git a/apps/api/.gitignore b/apps/api/.gitignore
index a983ad35..1ba22ba8 100755
--- a/apps/api/.gitignore
+++ b/apps/api/.gitignore
@@ -46,3 +46,5 @@ package-lock.json
 # IDE files
 /.idea
 
+#uploads
+src/uploads/avatars
\ No newline at end of file
diff --git a/apps/api/package.json b/apps/api/package.json
index 1a9f00d1..924b52c0 100755
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -17,8 +17,11 @@
   "dependencies": {
     "@diary-spo/crypto": "workspace:*",
     "@elysiajs/cors": "^1.1.1",
+    "@elysiajs/static": "^1.1.1",
+    "@types/cli-progress": "^3.11.6",
     "@types/node-telegram-bot-api": "^0.64.7",
     "cache-manager": "^5.7.6",
+    "cli-progress": "^3.12.0",
     "elysia": "1.1.21",
     "elysia-compression": "^0.0.7",
     "elysia-helmet": "^2.0.0",
diff --git a/apps/api/src/helpers/syncDatabaseForDecorations.ts b/apps/api/src/helpers/syncDatabaseForDecorations.ts
new file mode 100644
index 00000000..a41ab7ae
--- /dev/null
+++ b/apps/api/src/helpers/syncDatabaseForDecorations.ts
@@ -0,0 +1,108 @@
+import {AvatarModel} from "../models/Avatar";
+import {readdir} from "node:fs/promises";
+import cliProgress from "cli-progress"
+import {AvatarTagModel} from "../models/AvatarTag";
+import {TagModel} from "../models/Tag";
+
+const pathToDecorationsFile = 'src/uploads/decorations.json'
+const pathToAvatars = 'src/uploads/avatars'
+
+interface Content {
+	avatars: AvatarContent[]
+}
+
+interface AvatarContent {
+	isAnimated: boolean
+	filename: string
+	tags: string[]
+	price: number
+}
+
+export const syncDatabaseForDecorations = async () => {
+	console.log('Обновляю декорации в базе данных...')
+
+	let progress = 0
+	const progressBar = new cliProgress.SingleBar({
+		format: ' {bar} | {filename} | {value}/{total}'
+	}, cliProgress.Presets.shades_classic)
+
+	const file = Bun.file(pathToDecorationsFile)
+
+	if (!await file.exists()) {
+		console.log('Файл декораций не найден. Пропускаю ...')
+		return
+	}
+
+	const contents: Content = await file.json()
+	const avatarFiles = await readdir(pathToAvatars)
+
+	progressBar.start(avatarFiles.length, progress)
+
+	for (const avatarFile of avatarFiles) {
+		progressBar.increment(1, {filename: avatarFile})
+		const avatarToSave = {
+			filename: avatarFile,
+			isAnimated: avatarFile.endsWith('.gif'),
+			price: 1000
+		}
+
+		let tags: string[] = []
+
+		// Пробуем найти, переопределена ли цена и статус анимации
+		for (const avatarInfo of contents.avatars) {
+			if (avatarInfo.filename !== avatarFile)
+				continue
+			avatarToSave.isAnimated = avatarInfo.isAnimated
+			avatarToSave.price = avatarInfo.price
+			tags = avatarInfo.tags
+			break
+		}
+
+		// Находим или создаём модель
+		const avatarModel = await AvatarModel.findOrCreate({
+			where: {
+				filename: avatarFile,
+			},
+			defaults: avatarToSave
+		})
+
+		// Проверяем наличие тегов
+		if (!tags)
+			await AvatarTagModel.destroy({
+				where: {
+					avatarId: avatarModel[0].id
+				}
+			})
+		else
+			for (const tag of tags) {
+				const tagModel = await TagModel.findOrCreate({
+					where: {
+						value: tag
+					},
+					defaults: {
+						value: tag
+					}
+				})
+				await AvatarTagModel.findOrCreate({
+					where: {
+						avatarId: avatarModel[0].id,
+						tagId: tagModel[0].id
+					},
+					defaults: {
+						avatarId: avatarModel[0].id,
+						tagId: tagModel[0].id
+					}
+				})
+			}
+
+		// Если создана - то завершаем цикл
+		if (avatarModel[1])
+			continue
+
+		await avatarModel[0].update(avatarToSave)
+	}
+
+	progressBar.stop()
+
+	console.log('Обновил все декорации')
+}
\ No newline at end of file
diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts
index f5dabae0..f8b40751 100755
--- a/apps/api/src/main.ts
+++ b/apps/api/src/main.ts
@@ -11,6 +11,8 @@ import { getTimezone } from './config/getTimeZone'
 import { routes } from './routes'
 
 import './models/relations'
+import staticPlugin from "@elysiajs/static";
+import {syncDatabaseForDecorations} from "./helpers/syncDatabaseForDecorations";
 
 // настраиваем сервер...
 const port = Bun.env.PORT ?? 3003
@@ -53,6 +55,10 @@ const app = new Elysia()
       contentSecurityPolicy: false
     })
   )
+  .use(staticPlugin({
+    assets: 'src/uploads',
+    prefix: 'uploads'
+  }))
   .use(routes)
   .listen(port)
 
@@ -69,6 +75,8 @@ console.log(
   }.`
 )
 
+await syncDatabaseForDecorations()
+
 export type App = typeof app
 const workerURL = new URL('worker', import.meta.url).href
 new Worker(workerURL)
diff --git a/apps/api/src/models/Avatar/index.ts b/apps/api/src/models/Avatar/index.ts
new file mode 100644
index 00000000..116e6686
--- /dev/null
+++ b/apps/api/src/models/Avatar/index.ts
@@ -0,0 +1 @@
+export * from './model'
diff --git a/apps/api/src/models/Avatar/model.ts b/apps/api/src/models/Avatar/model.ts
new file mode 100644
index 00000000..44e4db23
--- /dev/null
+++ b/apps/api/src/models/Avatar/model.ts
@@ -0,0 +1,42 @@
+import { DataTypes } from 'sequelize'
+
+import { cache, enableCache, sequelize } from '@db'
+
+import { SPOModel } from '../SPO'
+import type { IModelPrototype } from '../types'
+
+// REMOVE IT
+// ?
+export type AvatarModelType = {
+  id: bigint
+  filename: string
+  price: number
+  isAnimated: boolean
+}
+
+export type IAvatarModelType = IModelPrototype<AvatarModelType, 'id'>
+
+const avatarModel = sequelize.define<IAvatarModelType>('avatar', {
+  id: {
+    type: DataTypes.BIGINT,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  filename: {
+    type: DataTypes.STRING(35),
+    allowNull: false
+  },
+  price: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    defaultValue: 1000
+  },
+  isAnimated: {
+    type: DataTypes.BOOLEAN,
+    allowNull: false
+  }
+})
+
+export const AvatarModel = enableCache
+  ? cache.init<IAvatarModelType>(avatarModel)
+  : avatarModel
diff --git a/apps/api/src/models/AvatarTag/index.ts b/apps/api/src/models/AvatarTag/index.ts
new file mode 100644
index 00000000..116e6686
--- /dev/null
+++ b/apps/api/src/models/AvatarTag/index.ts
@@ -0,0 +1 @@
+export * from './model'
diff --git a/apps/api/src/models/AvatarTag/model.ts b/apps/api/src/models/AvatarTag/model.ts
new file mode 100644
index 00000000..d1047b7f
--- /dev/null
+++ b/apps/api/src/models/AvatarTag/model.ts
@@ -0,0 +1,40 @@
+import { DataTypes } from 'sequelize'
+
+import { cache, enableCache, sequelize } from '@db'
+
+import { SPOModel } from '../SPO'
+import type {IModelPrototype, IModelPrototypeNoId} from '../types'
+import {AvatarModel} from "../Avatar";
+import {TagModel} from "../Tag";
+
+// REMOVE IT
+// ?
+export type AvatarTagModelType = {
+  avatarId: bigint
+  tagId: bigint
+}
+
+export type IAvatarTagModelType = IModelPrototypeNoId<AvatarTagModelType>
+
+const avatarTagModel = sequelize.define<IAvatarTagModelType>('avatarTag', {
+  avatarId: {
+    type: DataTypes.BIGINT,
+    primaryKey: true,
+    allowNull: false,
+    references: {
+      model: AvatarModel
+    }
+  },
+  tagId: {
+    type: DataTypes.BIGINT,
+    primaryKey: true,
+    allowNull: false,
+    references: {
+      model: TagModel
+    }
+  }
+})
+
+export const AvatarTagModel = enableCache
+  ? cache.init<IAvatarTagModelType>(avatarTagModel)
+  : avatarTagModel
diff --git a/apps/api/src/models/DiaryUser/model.ts b/apps/api/src/models/DiaryUser/model.ts
index a00012a8..f0200c4b 100755
--- a/apps/api/src/models/DiaryUser/model.ts
+++ b/apps/api/src/models/DiaryUser/model.ts
@@ -7,6 +7,7 @@ import { formatDate } from '@utils'
 
 import { GroupModel } from '../Group'
 import type { IModelPrototype } from '../types'
+import {AvatarModel} from "../Avatar";
 
 // REMOVE IT
 // ?
@@ -26,6 +27,7 @@ export type DiaryUserModelType = {
   termStartDate?: Nullable<string>
   isAdmin: boolean
   idFromDiary: number
+  avatarId: bigint
 }
 
 export type IDiaryUserModel = IModelPrototype<DiaryUserModelType, 'id'>
@@ -125,6 +127,13 @@ export const DiaryUserModel = sequelize.define<IDiaryUserModel>('diaryUser', {
     defaultValue: false,
     comment: 'Признак администратора'
   },
+  avatarId: {
+    type: DataTypes.BIGINT,
+    allowNull: true,
+    references: {
+      model: AvatarModel
+    }
+  },
   idFromDiary: {
     type: DataTypes.INTEGER,
     allowNull: false,
diff --git a/apps/api/src/models/Tag/index.ts b/apps/api/src/models/Tag/index.ts
new file mode 100644
index 00000000..116e6686
--- /dev/null
+++ b/apps/api/src/models/Tag/index.ts
@@ -0,0 +1 @@
+export * from './model'
diff --git a/apps/api/src/models/Tag/model.ts b/apps/api/src/models/Tag/model.ts
new file mode 100644
index 00000000..80cecc0f
--- /dev/null
+++ b/apps/api/src/models/Tag/model.ts
@@ -0,0 +1,33 @@
+import { DataTypes } from 'sequelize'
+
+import { cache, enableCache, sequelize } from '@db'
+
+import { SPOModel } from '../SPO'
+import type { IModelPrototype } from '../types'
+import {AvatarModel} from "../Avatar";
+
+// REMOVE IT
+// ?
+export type TagModelType = {
+  id: bigint
+  value: string
+}
+
+export type ITagModelType = IModelPrototype<TagModelType, 'id'>
+
+const tagModel = sequelize.define<ITagModelType>('tag', {
+  id: {
+    type: DataTypes.BIGINT,
+    primaryKey: true,
+    autoIncrement: true,
+    allowNull: false
+  },
+  value: {
+    type: DataTypes.STRING,
+    allowNull: false
+  }
+})
+
+export const TagModel = enableCache
+  ? cache.init<ITagModelType>(tagModel)
+  : tagModel
diff --git a/apps/api/src/models/UserAvatar/index.ts b/apps/api/src/models/UserAvatar/index.ts
new file mode 100644
index 00000000..116e6686
--- /dev/null
+++ b/apps/api/src/models/UserAvatar/index.ts
@@ -0,0 +1 @@
+export * from './model'
diff --git a/apps/api/src/models/UserAvatar/model.ts b/apps/api/src/models/UserAvatar/model.ts
new file mode 100644
index 00000000..7db748b7
--- /dev/null
+++ b/apps/api/src/models/UserAvatar/model.ts
@@ -0,0 +1,38 @@
+import { DataTypes } from 'sequelize'
+
+import { cache, enableCache, sequelize } from '@db'
+
+import { SPOModel } from '../SPO'
+import type {IModelPrototype, IModelPrototypeNoId} from '../types'
+import {AvatarModel} from "../Avatar";
+import {DiaryUserModel} from "../DiaryUser";
+
+// REMOVE IT
+// ?
+export type UserAvatarModelType = {
+  avatarId: bigint
+  diaryUserId: bigint
+}
+
+export type IUserAvatarModelType = IModelPrototypeNoId<UserAvatarModelType>
+
+const userAvatarModel = sequelize.define<IUserAvatarModelType>('userAvatar', {
+  avatarId: {
+    type: DataTypes.BIGINT,
+    references: {
+      model: AvatarModel
+    },
+    allowNull: false
+  },
+  diaryUserId: {
+    type: DataTypes.BIGINT,
+    references: {
+      model: DiaryUserModel
+    },
+    allowNull: false
+  }
+})
+
+export const UserAvatarModel = enableCache
+  ? cache.init<IUserAvatarModelType>(userAvatarModel)
+  : userAvatarModel
diff --git a/apps/api/src/models/relations.ts b/apps/api/src/models/relations.ts
index 1d2c3497..a4b5a82d 100755
--- a/apps/api/src/models/relations.ts
+++ b/apps/api/src/models/relations.ts
@@ -28,6 +28,10 @@ import { TermSubjectExaminationTypeModel } from './TermSubjectExaminationType'
 import { TermTypeModel } from './TermType'
 import { TermUserModel } from './TermUser'
 import { ThemeModel } from './Theme'
+import {AvatarModel} from "./Avatar";
+import {UserAvatarModel} from "./UserAvatar";
+import {AvatarTagModel} from "./AvatarTag";
+import {TagModel} from "./Tag";
 
 // SPO <--->> Group
 SPOModel.hasMany(GroupModel)
@@ -193,10 +197,27 @@ TermSubjectModel.belongsTo(TeacherModel)
 DiaryUserModel.hasMany(TermSubjectModel)
 TermSubjectModel.belongsTo(DiaryUserModel)
 
+// DiaryUser <-->> Subscribe
 DiaryUserModel.hasMany(SubscribeModel)
 SubscribeModel.belongsTo(DiaryUserModel)
 
+// Avatar <-->> UserAvatar
+AvatarModel.hasMany(UserAvatarModel)
+UserAvatarModel.belongsTo(AvatarModel)
+
+// Avatar <-->> DiaryUser
+AvatarModel.hasMany(DiaryUserModel)
+DiaryUserModel.belongsTo(AvatarModel)
+
+// Avatar <-->> AvatarTag
+AvatarModel.hasMany(AvatarTagModel)
+AvatarTagModel.belongsTo(AvatarModel)
+
+// Tag <-->> AvatarTag
+TagModel.hasMany(AvatarTagModel)
+AvatarTagModel.belongsTo(TagModel)
+
 if (forceSyncDatabase) {
   console.log('Syncing database...')
-  await sequelize.sync({ alter: true })
+  await sequelize.sync()
 }
diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts
index 4d730f8b..1b3f61b4 100755
--- a/apps/api/src/routes/index.ts
+++ b/apps/api/src/routes/index.ts
@@ -10,6 +10,7 @@ import { HomeController } from './home'
 import { LessonsController } from './lessons'
 import { OrganizationController } from './organization'
 import { PerformanceCurrentController } from './performance.current'
+import {MarketAvatars} from "./marketAvatars";
 
 export const routes = new Elysia()
   .use(HomeController)
@@ -20,6 +21,7 @@ export const routes = new Elysia()
   .use(AttestationController)
   .use(FinalMarksController)
   .use(PerformanceCurrentController)
+  .use(MarketAvatars)
   /** Обработка любых ошибок в кажом роуте **/
   .onError(({ set, code, path, error }) => {
     if (Number(code)) {
diff --git a/apps/api/src/routes/marketAvatars/handler.ts b/apps/api/src/routes/marketAvatars/handler.ts
new file mode 100644
index 00000000..cb6812bb
--- /dev/null
+++ b/apps/api/src/routes/marketAvatars/handler.ts
@@ -0,0 +1,45 @@
+import {AvatarModel, AvatarModelType, IAvatarModelType} from "../../models/Avatar";
+import {AvatarData} from "@diary-spo/shared";
+import {ITagModelType, TagModel, TagModelType} from "../../models/Tag";
+import {AvatarTagModel, AvatarTagModelType, IAvatarTagModelType} from "../../models/AvatarTag";
+import {sequelize} from "@db";
+
+interface MarketAvatarsParams {
+	page: number
+}
+
+type IAvatarsFromDB = IAvatarModelType & {
+	avatarTags: (IAvatarTagModelType & {
+		tag: ITagModelType
+	})[]
+}
+
+const elementsForPage = 100
+
+const getMarketAvatars = async ({page}: MarketAvatarsParams): Promise<AvatarData[]> => {
+	const avatars = await AvatarModel.findAll({
+		limit: elementsForPage,
+		offset: elementsForPage * (page - 1),
+		include: [
+			{
+				model: AvatarTagModel,
+				include: [TagModel]
+			}
+		],
+		order: [[sequelize.literal('id'), 'ASC']]
+	}) as IAvatarsFromDB[]
+
+	const formattedResult: AvatarData[] = []
+
+	for (const avatar of avatars)
+		formattedResult.push({
+			filename: avatar.filename,
+			tags: avatar.avatarTags.map(avatarTag => avatarTag.tag.value),
+			isAnimated: avatar.isAnimated,
+			price: avatar.price
+		})
+
+	return formattedResult
+}
+
+export default getMarketAvatars
diff --git a/apps/api/src/routes/marketAvatars/index.ts b/apps/api/src/routes/marketAvatars/index.ts
new file mode 100644
index 00000000..3776280a
--- /dev/null
+++ b/apps/api/src/routes/marketAvatars/index.ts
@@ -0,0 +1,17 @@
+import { Elysia, t } from 'elysia'
+import { AuthPlugin } from '../../services/AuthService'
+import getLessons from './handler'
+import getMarketAvatars from "./handler";
+
+export const MarketAvatars = new Elysia()
+  .use(AuthPlugin)
+  .get(
+    '/marketAvatars/:page',
+    ({ params: { page }, Auth: { user } }) =>
+      getMarketAvatars({page}),
+    {
+      params: t.Object({
+        page: t.Numeric({minimum: 1, maximum: 999})
+      })
+    }
+  )
diff --git a/apps/api/src/uploads/decorations.json b/apps/api/src/uploads/decorations.json
new file mode 100644
index 00000000..5f80e6ee
--- /dev/null
+++ b/apps/api/src/uploads/decorations.json
@@ -0,0 +1,16 @@
+{
+  "avatars": [
+    {
+      "isAnimated": true,
+      "filename": "1.gif",
+      "tags": ["Девушка"],
+      "price": 2500
+    },
+    {
+      "isAnimated": false,
+      "filename": "2.jpg",
+      "tags": ["Парень", "Эпично"],
+      "price": 1500
+    }
+  ]
+}
\ No newline at end of file
diff --git a/apps/shared/src/api/self/index.ts b/apps/shared/src/api/self/index.ts
index 8d0adb74..a86434c3 100755
--- a/apps/shared/src/api/self/index.ts
+++ b/apps/shared/src/api/self/index.ts
@@ -30,3 +30,10 @@ export type ResponseLogin = Person & {
   token: string
   tokenId: bigint
 }
+
+export interface AvatarData {
+  isAnimated: boolean
+  filename: string
+  tags: string[]
+  price: number
+}
\ No newline at end of file
diff --git a/apps/web/.env.development b/apps/web/.env.development
index 144984b3..6f1a9c3e 100755
--- a/apps/web/.env.development
+++ b/apps/web/.env.development
@@ -1,7 +1,6 @@
 VITE_SERVER_URLS=http://localhost:3003,http://127.0.0.1:3003
 VITE_SERVER_URL=http://localhost:3003
-# http://localhost:3003,http://127.0.0.1:3003,
-VITE_TG_BOT_URL=https://t.me/diary_notifications_bot
+VITE_TG_BOT_URL=IGNORE
 VITE_MODE=dev
 VITE_NODE_ENV=development
 NODE_ENV=development
diff --git a/apps/web/src/app/AppWrapper/App/ModalRoot/modals/UserEditModal/index.tsx b/apps/web/src/app/AppWrapper/App/ModalRoot/modals/UserEditModal/index.tsx
index b476d1bf..06894202 100644
--- a/apps/web/src/app/AppWrapper/App/ModalRoot/modals/UserEditModal/index.tsx
+++ b/apps/web/src/app/AppWrapper/App/ModalRoot/modals/UserEditModal/index.tsx
@@ -1,16 +1,22 @@
 import {
-  Avatar, Div,
+  Avatar,
+  Div,
   Flex,
   Group,
   Header,
   ModalPage,
-  ModalPageHeader, Text
+  ModalPageHeader,
+  Text
 } from '@vkontakte/vkui'
 import './index.css'
-import {Icon16DoneCircle, Icon28ShoppingCartOutline, Icon56MarketOutline} from '@vkontakte/icons'
+import {
+  Icon16DoneCircle,
+  Icon28ShoppingCartOutline,
+  Icon56MarketOutline
+} from '@vkontakte/icons'
+import { useRouteNavigator } from '@vkontakte/vk-mini-apps-router'
 import { useState } from 'react'
-import {useRouteNavigator} from "@vkontakte/vk-mini-apps-router";
-import {PAGE_MARKET} from "../../../../../routes";
+import { PAGE_MARKET } from '../../../../../routes'
 
 const urls = [
   'https://mangabuff.ru/img/avatars/x150/806.gif',
@@ -106,7 +112,7 @@ const UserEditModal = ({ id }: { id: string }) => {
             ))}
             <Avatar size={110} onClick={openMarket}>
               <Flex direction='column' align='center'>
-                <Icon28ShoppingCartOutline height={50} width={50}/>
+                <Icon28ShoppingCartOutline height={50} width={50} />
                 <Text>Купить ещё</Text>
               </Flex>
             </Avatar>
diff --git a/apps/web/src/pages/Market/components/avatarsPanel.tsx b/apps/web/src/pages/Market/components/avatarsPanel.tsx
index 1beca268..1778fff0 100644
--- a/apps/web/src/pages/Market/components/avatarsPanel.tsx
+++ b/apps/web/src/pages/Market/components/avatarsPanel.tsx
@@ -1,74 +1,99 @@
-import {Avatar, Flex, Group, Header, Placeholder, ToolButton, Tooltip} from "@vkontakte/vkui";
-import {Icon16DiamondOutline, Icon16Gift, Icon56TagOutline} from "@vkontakte/icons";
-import {FC, useState} from "react";
-import {type AvatarData} from './types.tsx'
+import type { AvatarData } from '@diary-spo/shared'
+import {
+  Icon16DiamondOutline,
+  Icon16Gift,
+  Icon56TagOutline
+} from '@vkontakte/icons'
+import {
+  Avatar,
+  Flex,
+  Group,
+  Header,
+  Placeholder,
+  ToolButton,
+  Tooltip
+} from '@vkontakte/vkui'
+import { type FC, useState } from 'react'
+import { API_URL } from '../../../shared/config'
 
 interface Props {
-	avatars: AvatarData[],
-	isStatic: boolean,
-	isAnimated: boolean,
-	selectedTags: string[]
+  avatars: AvatarData[]
+  isStatic: boolean
+  isAnimated: boolean
+  selectedTags: string[]
 }
 
-const url = 'https://mangabuff.ru/img/avatars/x150'
+const url = `${API_URL}/uploads/avatars/`
 
-const avatarsPanel: FC<Props> = ({avatars, isStatic, isAnimated, selectedTags}) => {
-	const [isNotZeroElements, setIsNotZeroElements] = useState(true)
+const avatarsPanel: FC<Props> = ({
+  avatars,
+  isStatic,
+  isAnimated,
+  selectedTags
+}) => {
+  const [isNotZeroElements, setIsNotZeroElements] = useState(true)
 
-	const getUrlPath = (avatarData: AvatarData) => {
-		return `${url}/${avatarData.filename}`
-	}
+  const getUrlPath = (avatarData: AvatarData) => {
+    return `${url}/${avatarData.filename}`
+  }
 
-	const filteredAvatars = () => {
-		const filtered = avatars.filter(avatar => {
-			const isType = (!isAnimated && !isStatic) || (avatar.isAnimated && isAnimated || !avatar.isAnimated && isStatic)
-			const isTags = !selectedTags.length || selectedTags.filter(tag => avatar.tags.includes(tag)).length == avatar.tags.length
-			return isType && isTags
-		})
+  const filteredAvatars = () => {
+    const filtered = avatars.filter((avatar) => {
+      const isType =
+        (!isAnimated && !isStatic) ||
+        (avatar.isAnimated && isAnimated) ||
+        (!avatar.isAnimated && isStatic)
+      const isTags =
+        !selectedTags.length ||
+        selectedTags.filter((tag) => avatar.tags.includes(tag)).length ===
+          selectedTags.length
+      return isType && isTags
+    })
 
-		const isNotZero = filtered.length > 0
+    const isNotZero = filtered.length > 0
 
-		if (isNotZeroElements != isNotZero)
-			setIsNotZeroElements(isNotZero)
+    if (isNotZeroElements !== isNotZero) setIsNotZeroElements(isNotZero)
 
-		return filtered
-	}
-	return (
-		<Group header={<Header>Полка аватарок</Header>}>
-			<Flex margin='auto' gap='2xl' justify='center'>
-				{filteredAvatars().map((avatar, index) => (
-						<Avatar key={index} size={110} src={getUrlPath(avatar)}>
-							<Avatar.Badge className='select-avatar_badge'>
-								<Tooltip title={avatar.price ? `${avatar.price} алмазов` : 'Бесплатно'}>
-									<ToolButton
-										IconCompact={avatar.price ? Icon16DiamondOutline : Icon16Gift}
-										IconRegular={avatar.price ? Icon16DiamondOutline : Icon16Gift}
-									>
-										{avatar.price || null}
-									</ToolButton>
-								</Tooltip>
-							</Avatar.Badge>
-						</Avatar>
-					)
-				)}
-				{
-					!isNotZeroElements && (
-							<Placeholder>
-								<Placeholder.Icon>
-									<Icon56TagOutline/>
-								</Placeholder.Icon>
-								<Placeholder.Title>
-									Ничего не найдено
-								</Placeholder.Title>
-								<Placeholder.Description>
-									Нет ни одной аватарки, удовлетворяющей указанным фильтрам
-								</Placeholder.Description>
-							</Placeholder>
-					)
-				}
-			</Flex>
-		</Group>
-	)
+    return filtered
+  }
+  return (
+    <Group header={<Header>Полка аватарок</Header>}>
+      <Flex margin='auto' gap='2xl' justify='center'>
+        {filteredAvatars().map((avatar, index) => (
+          <Avatar
+            key={index}
+            size={110}
+            src={getUrlPath(avatar)}
+            style={{ isolation: 'auto' }}
+          >
+            <Avatar.Badge className='select-avatar_badge'>
+              <Tooltip
+                title={avatar.price ? `${avatar.price} алмазов` : 'Бесплатно'}
+              >
+                <ToolButton
+                  IconCompact={avatar.price ? Icon16DiamondOutline : Icon16Gift}
+                  IconRegular={avatar.price ? Icon16DiamondOutline : Icon16Gift}
+                >
+                  {avatar.price || null}
+                </ToolButton>
+              </Tooltip>
+            </Avatar.Badge>
+          </Avatar>
+        ))}
+        {!isNotZeroElements && (
+          <Placeholder>
+            <Placeholder.Icon>
+              <Icon56TagOutline />
+            </Placeholder.Icon>
+            <Placeholder.Title>Ничего не найдено</Placeholder.Title>
+            <Placeholder.Description>
+              Нет ни одной аватарки, удовлетворяющей указанным фильтрам
+            </Placeholder.Description>
+          </Placeholder>
+        )}
+      </Flex>
+    </Group>
+  )
 }
 
-export default avatarsPanel
\ No newline at end of file
+export default avatarsPanel
diff --git a/apps/web/src/pages/Market/components/filtersPanel.tsx b/apps/web/src/pages/Market/components/filtersPanel.tsx
index 2e4fa781..c95fa14a 100644
--- a/apps/web/src/pages/Market/components/filtersPanel.tsx
+++ b/apps/web/src/pages/Market/components/filtersPanel.tsx
@@ -1,115 +1,109 @@
-import {Icon24PhotosStackOutline, Icon24VideoCircleOutline} from '@vkontakte/icons'
+import type { AvatarData } from '@diary-spo/shared'
 import {
-	Counter,
-	Group,
-	Header, Separator,
-	SubnavigationBar,
-	SubnavigationButton,
-	VisuallyHidden
+  Icon24PhotosStackOutline,
+  Icon24VideoCircleOutline
+} from '@vkontakte/icons'
+import {
+  Counter,
+  Group,
+  Header,
+  Separator,
+  SubnavigationBar,
+  SubnavigationButton,
+  VisuallyHidden
 } from '@vkontakte/vkui'
-import {FC} from 'react'
-import {AvatarData} from "./types.tsx";
+import type { FC } from 'react'
 
 interface Props {
-	avatars: AvatarData[],
-	isAnimated: boolean
-	isStatic: boolean
-	changeIsAnimated: () => void
-	changeIsStatic: () => void
-	setSelectedTags: (tags: string[]) => void
-	selectedTags: string[]
-	tags: string[]
+  avatars: AvatarData[]
+  isAnimated: boolean
+  isStatic: boolean
+  changeIsAnimated: () => void
+  changeIsStatic: () => void
+  setSelectedTags: (tags: string[]) => void
+  selectedTags: string[]
+  tags: string[]
 }
 
-const filtersPanel: FC<Props> = (
-	{
-		avatars,
-		isAnimated,
-		isStatic,
-		changeIsAnimated,
-		changeIsStatic,
-		setSelectedTags,
-		selectedTags,
-		tags
-	}) => {
-
-	const changeSelectTag = (tag: string) => {
-		const indexInSelected = selectedTags.indexOf(tag)
-
-		const selectedTagsCopy = selectedTags.slice(0)
+const filtersPanel: FC<Props> = ({
+  avatars,
+  isAnimated,
+  isStatic,
+  changeIsAnimated,
+  changeIsStatic,
+  setSelectedTags,
+  selectedTags,
+  tags
+}) => {
+  const changeSelectTag = (tag: string) => {
+    const indexInSelected = selectedTags.indexOf(tag)
 
-		if (indexInSelected > -1)
-			selectedTagsCopy.splice(indexInSelected, 1)
-		else selectedTagsCopy.push(tag)
+    const selectedTagsCopy = selectedTags.slice(0)
 
-		setSelectedTags(selectedTagsCopy)
+    if (indexInSelected > -1) selectedTagsCopy.splice(indexInSelected, 1)
+    else selectedTagsCopy.push(tag)
 
-		console.log(selectedTags)
-	}
+    setSelectedTags(selectedTagsCopy)
 
-	return (
-		<>
-			<Group header={<Header>Фильтры</Header>}>
+    console.log(selectedTags)
+  }
 
-				<SubnavigationBar>
-					<SubnavigationButton
-						before={<Icon24VideoCircleOutline/>}
-						selected={isAnimated}
-						onClick={changeIsAnimated}
-						mode='primary'
-						after={
-							<Counter size='s'>
-								<VisuallyHidden>Применено: </VisuallyHidden>
-								{avatars.filter(avatar => avatar.isAnimated).length}
-							</Counter>
-						}
-					>
-						Анимированные
-					</SubnavigationButton>
+  return (
+    <>
+      <Group header={<Header>Фильтры</Header>}>
+        <SubnavigationBar>
+          <SubnavigationButton
+            before={<Icon24VideoCircleOutline />}
+            selected={isAnimated}
+            onClick={changeIsAnimated}
+            mode='primary'
+            after={
+              <Counter size='s'>
+                <VisuallyHidden>Применено: </VisuallyHidden>
+                {avatars.filter((avatar) => avatar.isAnimated).length}
+              </Counter>
+            }
+          >
+            Анимированные
+          </SubnavigationButton>
 
-					<SubnavigationButton
-						before={<Icon24PhotosStackOutline/>}
-						selected={isStatic}
-						onClick={changeIsStatic}
-						mode='primary'
-						after={
-							<Counter size='s'>
-								<VisuallyHidden>Применено: </VisuallyHidden>
-								{avatars.filter(avatar => !avatar.isAnimated).length}
-							</Counter>
-						}
-					>
-						Статичные
-					</SubnavigationButton>
+          <SubnavigationButton
+            before={<Icon24PhotosStackOutline />}
+            selected={isStatic}
+            onClick={changeIsStatic}
+            mode='primary'
+            after={
+              <Counter size='s'>
+                <VisuallyHidden>Применено: </VisuallyHidden>
+                {avatars.filter((avatar) => !avatar.isAnimated).length}
+              </Counter>
+            }
+          >
+            Статичные
+          </SubnavigationButton>
 
-					{
-						tags.length && (
-							<Separator direction='vertical'/>
-						)
-					}
+          {tags.length && <Separator direction='vertical' />}
 
-					{
-						tags.map(tag => (
-							<SubnavigationButton
-								key={tag}
-								onClick={() => changeSelectTag(tag)}
-								selected={selectedTags.includes(tag)}
-								mode='primary'
-								after={
-									<Counter size='s'>
-										<VisuallyHidden>Применено: </VisuallyHidden>
-										{avatars.filter(avatar => avatar.tags.includes(tag)).length}
-									</Counter>
-								}
-							>
-								{tag}
-							</SubnavigationButton>
-						))
-					}
-				</SubnavigationBar>
-			</Group>
-		</>
-	)
+          {tags.map((tag) => (
+            <SubnavigationButton
+              key={tag}
+              onClick={() => changeSelectTag(tag)}
+              selected={selectedTags.includes(tag)}
+              mode='primary'
+              after={
+                <Counter size='s'>
+                  <VisuallyHidden>Применено: </VisuallyHidden>
+                  {avatars.filter((avatar) => avatar.tags.includes(tag)).length}
+                </Counter>
+              }
+            >
+              {tag}
+            </SubnavigationButton>
+          ))}
+        </SubnavigationBar>
+      </Group>
+    </>
+  )
 }
 
 export default filtersPanel
diff --git a/apps/web/src/pages/Market/components/marketHeader.tsx b/apps/web/src/pages/Market/components/marketHeader.tsx
index 405f1af3..f23e6632 100644
--- a/apps/web/src/pages/Market/components/marketHeader.tsx
+++ b/apps/web/src/pages/Market/components/marketHeader.tsx
@@ -1,4 +1,4 @@
-import {Icon28DiamondOutline} from '@vkontakte/icons'
+import { Icon28DiamondOutline } from '@vkontakte/icons'
 import {
   Avatar,
   Button,
@@ -51,7 +51,10 @@ const MarketHeader = () => {
         }
         actions={
           <ButtonGroup mode='horizontal' gap='s' stretched>
-            <Tooltip placement='right' description='История списания и зачисления кредитов'>
+            <Tooltip
+              placement='right'
+              description='История списания и зачисления кредитов'
+            >
               <Button mode='secondary' size='s'>
                 История операций
               </Button>
diff --git a/apps/web/src/pages/Market/components/types.tsx b/apps/web/src/pages/Market/components/types.tsx
deleted file mode 100644
index 7588c1e3..00000000
--- a/apps/web/src/pages/Market/components/types.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface AvatarData {
-	isAnimated: boolean
-	filename: string
-	tags: string[]
-	price: number
-}
\ No newline at end of file
diff --git a/apps/web/src/pages/Market/index.tsx b/apps/web/src/pages/Market/index.tsx
index be88740c..753bc01a 100644
--- a/apps/web/src/pages/Market/index.tsx
+++ b/apps/web/src/pages/Market/index.tsx
@@ -1,84 +1,80 @@
-import {Panel, View} from '@vkontakte/vkui'
-import {type FC, useState} from 'react'
-import {PanelHeaderWithBack} from '../../shared'
+import type { AvatarData } from '@diary-spo/shared'
+import { Panel, View } from '@vkontakte/vkui'
+import { type FC, useEffect, useState } from 'react'
+import { PanelHeaderWithBack } from '../../shared'
+import { client } from '../../shared/api/client.ts'
+import AvatarsPanel from './components/avatarsPanel.tsx'
 import Filters from './components/filtersPanel.tsx'
 import MarketHeader from './components/marketHeader.tsx'
-import AvatarsPanel from "./components/avatarsPanel.tsx";
 
-interface Props {
-}
+type Props = {}
+
+const Market: FC<Props> = () => {
+  const [activePanel] = useState('market')
 
-const avatars = [
-	{
-		isAnimated: true,
-		filename: '806.gif',
-		tags: ['Девушка'],
-		price: 0
-	},
-	{
-		isAnimated: false,
-		filename: '1209.gif',
-		tags: ['Парень'],
-		price: 250
-	},
-	{
-		isAnimated: false,
-		filename: '689.jpg',
-		tags: ['Девушка'],
-		price: 350
-	},
-	{
-		isAnimated: false,
-		filename: '688.jpg',
-		tags: ['Девушка'],
-		price: 1000
-	}
-]
+  // ФИЛЬТРЫ
+  const [isAnimated, setIsAnimated] = useState(true)
+  const [isStatic, setIsStatic] = useState(true)
+  const [selectedTags, setSelectedTags] = useState<string[]>([])
+  const [tags, setTags] = useState<string[]>([])
+  const [avatars, setAvatars] = useState<AvatarData[]>([])
 
-const tags: string[] = []
-avatars.forEach(avatar => {
-	avatar.tags.forEach((tag) => {
-		if (!tags.includes(tag))
-			tags.push(tag)
-	})
-})
+  const changeIsAnimated = () => {
+    setIsAnimated(!isAnimated)
+  }
 
-const Market: FC<Props> = () => {
-	const [activePanel] = useState('market')
+  const changeIsStatic = () => {
+    setIsStatic(!isStatic)
+  }
+
+  // Стучимся к серверу
+  const getAvatarsFromServer = async (): Promise<void> => {
+    const avatars = await client.marketAvatars({ page: 1 }).get()
+
+    const data = avatars.data
+
+    if (!data) return
+
+    const tagsToSave: string[] = []
 
-	// ФИЛЬТРЫ
-	const [isAnimated, setIsAnimated] = useState(true)
-	const [isStatic, setIsStatic] = useState(true)
-	const [selectedTags, setSelectedTags] = useState<string[]>([])
+    avatars.data.forEach((avatar) => {
+      avatar.tags.forEach((tag) => {
+        if (!tagsToSave.includes(tag)) tagsToSave.push(tag)
+      })
+    })
 
-	const changeIsAnimated = () => {
-		setIsAnimated(!isAnimated)
-	}
+    setTags(tagsToSave)
+    setAvatars(data)
+  }
 
-	const changeIsStatic = () => {
-		setIsStatic(!isStatic)
-	}
+  useEffect(() => {
+    getAvatarsFromServer()
+  }, [])
 
-	return (
-		<View activePanel={activePanel}>
-			<Panel id='market'>
-				<PanelHeaderWithBack title='Магазин'/>
-				<MarketHeader/>
-				<Filters isAnimated={isAnimated}
-								 isStatic={isStatic}
-								 changeIsAnimated={changeIsAnimated}
-								 changeIsStatic={changeIsStatic}
-								 avatars={avatars}
-								 setSelectedTags={setSelectedTags}
-								 selectedTags={selectedTags}
-								 tags={tags}/>
-				<AvatarsPanel avatars={avatars}
-											isStatic={isStatic}
-											isAnimated={isAnimated}
-											selectedTags={selectedTags}/>
-			</Panel>
-		</View>
-	)
+  return (
+    <View activePanel={activePanel}>
+      <Panel id='market'>
+        <PanelHeaderWithBack title='Магазин' />
+        <MarketHeader />
+        <Filters
+          isAnimated={isAnimated}
+          isStatic={isStatic}
+          changeIsAnimated={changeIsAnimated}
+          changeIsStatic={changeIsStatic}
+          avatars={avatars}
+          setSelectedTags={setSelectedTags}
+          selectedTags={selectedTags}
+          tags={tags}
+        />
+        <AvatarsPanel
+          avatars={avatars}
+          isStatic={isStatic}
+          isAnimated={isAnimated}
+          selectedTags={selectedTags}
+        />
+      </Panel>
+    </View>
+  )
 }
 
 export default Market
diff --git a/apps/web/src/pages/Settings/Actions/index.tsx b/apps/web/src/pages/Settings/Actions/index.tsx
index 325243b3..01408b6d 100755
--- a/apps/web/src/pages/Settings/Actions/index.tsx
+++ b/apps/web/src/pages/Settings/Actions/index.tsx
@@ -109,17 +109,19 @@ const Actions = () => {
         >
           Показывать тех. информацию
         </CellButton>
-        <CellButton
-          before={<Icon28Notifications />}
-          onClick={async () =>
-            window.open(
-              `${TG_BOT_URL}?text=/subscribe ${await getSecureToken()}`,
-              '_blank'
-            )
-          }
-        >
-          Подключить уведомления
-        </CellButton>
+        {TG_BOT_URL !== 'IGNORE' && (
+          <CellButton
+            before={<Icon28Notifications />}
+            onClick={async () =>
+              window.open(
+                `${TG_BOT_URL}?text=/subscribe ${await getSecureToken()}`,
+                '_blank'
+              )
+            }
+          >
+            Подключить уведомления
+          </CellButton>
+        )}
         <CellButton
           before={<Icon28DoorArrowRightOutline />}
           onClick={() => routeNavigator.showPopout(logOutPopup)}
diff --git a/apps/web/src/shared/config/constants.ts b/apps/web/src/shared/config/constants.ts
index ddad21df..4d28568f 100755
--- a/apps/web/src/shared/config/constants.ts
+++ b/apps/web/src/shared/config/constants.ts
@@ -3,4 +3,4 @@ export const THIRD_SEC = 30 * SECOND
 /** Modal's ids */
 export const MODAL_PAGE_LESSON = 'lesson'
 export const MODAL_PAGE_MARK = 'mark'
-export const MODAL_PAGE_USER_EDIT = 'user-edit'
\ No newline at end of file
+export const MODAL_PAGE_USER_EDIT = 'user-edit'
diff --git a/apps/web/src/widgets/recent-marks/ui/LessonGrades/index.tsx b/apps/web/src/widgets/recent-marks/ui/LessonGrades/index.tsx
index 02fad56b..0c6e4888 100755
--- a/apps/web/src/widgets/recent-marks/ui/LessonGrades/index.tsx
+++ b/apps/web/src/widgets/recent-marks/ui/LessonGrades/index.tsx
@@ -40,6 +40,7 @@ export const LessonGrades: FC<LessonGradesProps> = ({ day, lessonGrades }) => {
       <div className='flex'>
         {lessonGrades.map(({ lessonName, task }) => (
           <HorizontalCell
+            key={task.id}
             size='xl'
             onClick={() => handleMarkClick(task, lessonName)}
             className='markWrapper'
diff --git a/bun.lockb b/bun.lockb
index bb6f63098572327ddb4b085d8d431bcc9fa77668..7868480d3bc90ddc9ba71ad2fe24c58047077a24 100755
GIT binary patch
delta 6868
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

delta 5891
zcmeHLeN<Id7C-0eGcJOOA1Mk6D9A&2eCj39_f*u<G%#CBfkZ)`yqA1`DWeEl=67ai
z><DUUjUg1X7ULATmaaY~%Vw;c$xJPsq%siE$f<Oua(?^W^IFX;)@senTJu+DvDxSM
z+xzTu&c6HJckcb<qJHBq`r|h1TT??e9tqBvSlc~+chJM#w=NyER(_neaOBObi8C_>
z+tzisG)8Uzjs>H%y?a_$hWOTIWixgMV|DjK>(DEfuk`p-EjDpwr8|!Z7w`bsGnFoP
z7FIdS7;9f*`c>x?6u8SP*euus;5Qz62y_JW?a+nAh0dZ1#)5!-f%A$hD%?(1%krGL
zmHC*!4~Bs-FlayM9ZSu?g?Zy?e)gThOUtTD5Qi;w<&-&#mNM_cy%XMpjnD)om1WNS
zvYf)oqCEFfH)CPT%!QnHn)955_GhWA?if1#G2mqn!RnsGYG~!v(6p0A@T46oL+`oN
z1hi_j`^4G)qvDGnGQCz-IEyG@xxoGy^#C;GZKl#C`7!RIie#25tbSudErsT0msfj2
zu>{OPqnriR<?bBDLPV+Gu-Z*U=8`W*tCav#iUuq1Tg9f`do8Xh{Ouy-8-7Wp<{H*Q
z)3rE0(M)Y8?6fxTY@YyA4WuGnlo718qPoPHSRPYuI(j$x@0%;mtweSi^KNo<xybd8
z7__F++)YXqV?z-yrs`0G{{%mACUDB3<PjzFc&TVw?%>Bn=W>T$i|cNK6B9kb25%AW
z0*C$$xHxb=BK}T8k66OkbRaHTM;ZD<3f(43=O_CBB*6p#$1Wgjv96ye4Keg^<Z%K}
zUk}Oxnr(W{F!TdJ=|Gw&4K}o|b3}8Iolg>-MGmaZTI|q&n`;KvM9+LfZwE>+L%9q-
zM0CPq0qT+_>nnPu7`#EaOC0*U;Ak(JXbm-V3+jP9xF{WG=nf!?!9={(&`W`6aUR4Q
zMN_H6(he?N#AK%E2`Ifuo{`uwFB0xDht};B&1H5y9toce6IPy{EUe`Yy$#GX57QcF
z=r@7vrX9&LP^)C?E8@cp-Yl#Y4j+y>rcB6Ce;z0H1^MuMMN@@C-wB?Ui^wiRJL?ir
zm3DrIa929?JQN0n!A=nLIUq_p7x6YjKc^7Vj6DxPxhI=JLJdnD&}>l{nxg*^Ju`!}
z`|p6vc&*unkA1m$eSch#zD_Y%3NGZEK$I&kdeW190BC=hJk!u87MP|!gm{T)TIsOt
z1(z&ZDkt#*5mjy118}cR@hmAm!qDdd;dY{6xF|J1v=DQmSAfiowW3C@1I>Yr2`rL7
zAew&V;Ps;OR}TFH+=usgVjv<97VcFJzF0J^a%e5ZB5Ji=zrExq4*CM16uA3}o>)VF
zLfKF^D7|wfBC5vDrwMnBLwmebG}qWICvm$@6fLe4-4_p9RYA!6bRctiR8K2`@Pwp$
zB+<}X%EaYbyWRzo(vRzHH7qH(eW#0-*c81Mz1iUVcoO<Hkh$wtq_=;CnSRrj1{4QR
z<QLhh{n193?GHd^SyJ>{Kn_oIJMMSNFmBKphPI_rG_SMkuYyeQ2C{qwlp?z4rs(N-
zD9}=vr<NLksGM;*+=l)k5RK79e2AgxD-K0HGTS$2Fk|d>&X@-M2J}GaQ_%QhAJD&_
z(d2Sk`G2VV|3!=SkBzM*!&zk|P2F<}lkNk2Ug6#}t*Tquf2w`Je+`~Y-za-8+SlXo
zEgUqlX#3b&c(cC)-3HBR=SouqO<~d&;M<^y_fh<RqG_CtaWv0B6(>N$?=u+&senJ#
z#195f6Ay<bml60!0YV8VO@3htlO{YGx-axt#rLMkWYw^La*R=qqzT7EQ=kNBT7Usf
z7iJ<fHPW<@sS5X|Y5X*0?@g0E^)5Kjgm**JM03;t($u|AVbau1S2$fel=;YrUuyQ3
zn)TG~|Fve5QG@?g%^o`Z)avze+ZcW?UoX#&;S1!MG2CBH4hOM8ZVCsnF&soU5u4;)
z5g?KyK<teGu|;+g(LqE@B#3SDW+aGRV?ivkg4iybtRU`*1mPbAVu#F#0&#<gqeKX)
zj{|Yg3Zifvh@J8<5euV01V@9A(j5)LXB>!kh^Uu=F(6(hq9z8!ZuvG5u4oW3HV}Jd
zwGG7Z7!c=)cv@OxK^zCswk?)_6sUDBmKW^YhEq`Awmq|WN58g?bl#fg`^g@93NZHN
zKDlc@pL8et;g;EmTnHPPO&4I<v!4&1oS|rX9??UeTBhRYva~AyEXC1d<%r@QlvAHU
zh-^R_N{_I05NgYmJMoWr7{>AyrvtB7oJ;PY5N??0S=1l0RhgD6cR%25iYri@Kk#D^
z8dE4QB1CPaGU1hh&4W;@QXKutMUpSt3dIGW|2_J&p_S0I*+Gym(5JRq`O)wA6~)yk
zZV0$7id$oH1L&QB4F#a>)ylQcVq0~9H0?0R?;+IInf`1z`bQwN<%hx1{DF{#5NaFb
zpDDyf<vtQrn8(1_CdJ`xiG?d}GdP+m7&2CITV?2P5MsM>9|gJ$eQJ*@E(HA?#qCgB
zD7aw7Jt0degixl@Fuew$#s6AyW6;O>%F}j&qgBwVa6n?TYAIi%5cSGLi^FM&UiH{+
z6(SOSD})wLHH<$Gj#AuS8E^n0o>Hc9pt~WIkEa!f(=olocxK+GxENpqLT$gyp%Bj~
zQ!J>N=+g|(LervgfMgNqQ#+vi;?chfp(&q}tq2j(sN54l#iCEcniLnZA04z4jq>sV
z{-oUc96y<nir~{BGa&TSPruSsjdZRU0||#jKyJs_fsg<Qol)puLx-t<Kz@MSg4~2$
zg<OMtDO(%)U<)1VF35Kp`N*Li=ypObL%JYTx35dfK`!}&vi=}Xn@cBwxez*Xq(CM?
zCPVBH2V@Fl9E8q3I8b?h5b1b#148FPI%Cqg@*~K{az+zx)bp@ky!o;vva^Yg9!h76
zGzcAHVj*<QJ1xVT`39aVo0|C-zI1%~4&%S)vf+8YEYJ$v2htZ}f%JoX0Q)H!_yTY9
zy+H{_OKzo{hP~9nU+p)Ps%aPGG00TNR>;P-i-&j{52b3_0Q@LqJp|t!Jhu$pvDDuz
zkG{x<)ouga1GpQ~0I7%2<C1O-a--X(0wN(#La5&9K6nB`w+hvI4*aNKX)G0U1EjZ@
zRJnU0RGCz<u2xP@ORTxO?V*?W*63ll9qNW2O#1Nd9b5ducy^p^LXyp3gXPpudEiLz
zCy&&9pWe7I`Rw{I9&d}cC7?J%<aRjl1ljZ{&*u&q@EK1W>HVnGr%(T97QOIvGUCPC
z;%sogUoQR(?pd-Le*AvfLWs$EUqJcEi=XkiJV?fV4izD5J_k8ohIT;BlC7U(K&C7v
z<dU}tRmr0rJW(#Y#7AiDOXRvsd>MWtJ1_CMBfX!$>f7hE@0@UAyvEniit#@DKTS55
zH_HB<NQn3ASH|hUV+F4~+NAO9BwL~l&y{1+0f!*-dl{>HxGHPy`@6OtheLub*@k=d
zyi-<m@;N+Az6M7P@Z(NCJk9(4tZeq-tI=m-p4X7#IPz!SFKihHyWX0<|HKN7Z_YQr
zXnDV_@zvpPEs8kk!X(&bA`<OM+Youai;v=|^7}3%^gem-733~U{`E4@Oc~RKeR#jO
z%{m#RKe+1j8c&`))8@(?IPhY*iU$8oyg<3;3Ln8Ovi=Hx5mk`Z&2b!&tGanHPCi}T
zyxP7GU)k`hSyzL)Bi*>E+ZmA)c{kQP-mDGpcev8%%~!)N3_9h>DQxuBR35p?hikWr
z<>{+DfhS4*8Wy!yF1UvC%kQr6VRGpS9Kyc&k|$xi<9c|aHo9Cc>EXl2dq1+hvGLTq
znSVPOh#e-H^YyZKvS7Yk+0uh+=>2|IHtkP!tUP-a7Ni_v%4K^GpQEj*lyP72j-VL4
zfe*l@UdcbV`@O@PqCFSlF57rK<EmuRbw0=UjC#3Uc&%;ob^a#z-AGSRw6~YFWq!@?
z%$B?7Ymsg5&DWX&+73IlMVicZX?k0*TMM)J`sCx0gcg}EXBBDTvZP20@hwqgoJ%$o
kX+QWbahXnaE^>;H$BVU){>K%GkJ)m|bv{D+muT<*6RTin>Hq)$