Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_AUTH_API_BASE_URL=https://api.test.profcomff.com
VITE_TVOI_FF_TOKEN=null
133 changes: 58 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,111 +1,94 @@
# Мини-приложение рейтинга преподавателей
# Rating UI -- интерфейс сервиса оценки преподавателей "Дубинушка"

[<img src="https://cdn.profcomff.com/easycode/easycode.svg" width="200"></img>](https://easycode.profcomff.com/templates/docker-node/workspace?mode=manual&param.Repository+URL=https://github.com/profcomff/rating-ui.git&param.Working+directory=rating-ui)
Фронтенд для приложения по оценке преподавателей на Vue.js с TypeScript и Vite.

В этом репозитории представлен пример простейшего фронтенд-сервиса с Vue.js.
Большая часть команд и инструкций написаны для Linux и MacOS, но должны работать и на Windows.


*Большая часть команд и инструкций написаны для операционных систем Linux и MacOS.*
---

## Зависимости
- Node.js v18 – среда исполнения на языке JavaScript/TypeScript
- NPM – консольный менеджер пакетов для установки библиотек (идет в поставке с Node.js)
- Vue.js – фраемворк для разработки фронтенда
- Vite – консольный менеджер для удобства работы с Vue.js

## Frontend разработка на Vue.js
- **Node.js** >= v18 – среда выполнения JavaScript/TypeScript
- **PNPM** – менеджер пакетов (можно использовать NPM, но мы рекомендуем PNPM)
- **Vue.js** – фреймворк для фронтенда
- **Vite** – инструмент сборки и локального сервера
- **Docker** – для сборки и деплоя

Фронтенд (англ. front end, frontend) — презентационная часть web приложения, её пользовательский
интерфейс и связанные с ним компоненты.
---

В данном примере используется популярный фраемворк [Vue.js](https://vuejs.org/). Разработка ведется
на языке TypeScript.
## Разработка

1. Склонируй проект:
1. `cd /path/to/folder`
2. `git clone https://github.com/profcomff/rental-ui.git project-folder-name`

## Разработка
2. Установи зависимости: `pnpm install`

Для удобства разработки в VS Code создан [workspace](../frontend.code-workspace) с преднастроенными
командами и рекомендованными расширениями для работы.
3. Запусти локальный dev-сервер: `pnpm dev`

Перед началом работы нужно установить зависимости командой
```
pnpm install
```
Приложение будет доступно на http://localhost:5173.

Для локального запуска необходимо выполнить команду
```
pnpm dev
```
### Форматирование

1. Открыть файл с расширением `.vue`.
2. Открыть палитру команд (Help > Show All Commands или `Ctrl+Shift+P`)
3. Ввести и выбрать `Format document with`.
4. Выбрать `Configure Default Formatter`.
5. Выбрать `Prettier`.

### Важные замечания по коду
- Приложение предполагает, что вы запускаете его из Твой ФФ. Чтобы имитировать запуск из Твой ФФ:
Теперь можно форматировать файлы с помощью `Shift+Alt+F`. Еще можно настроить автоформатирование при сохранении файла (`File > Preferences > Settings`, Format on save).

1. Зарегистрируйтесь в тестовой среде «Твой ФФ!» по адресу https://app.test.profcomff.com/auth. Подтвердите аккаунт и войдите в пользователя (при необходимости).
### Локальная авторизация

2. Перейдите в панель администрирования https://app.test.profcomff.com/admin.
Для разработки с определенными скоупами можно проделать следующие шаги:

3. Нажмите кнопку «скопировать параметры приложения».
1. Переходим в [тестовое приложение](https://app.test.profcomff.com/admin) и копируем токен.
2. Вставляем токен в .env.development
3. В `/src/store/profileStore.ts` в функции `setupDevAdminSession` проверяем, есть ли все нужные нам скоупы.
4. В `src/App.vue` в хуке `onMounted`:

4. Подставьте полученную строку после адреса вашего приложения в браузере
```typescript
if (import.meta.env.MODE === 'development') await profileStore.setupDevAdminSession(null);
```

Код, который обрабатывает данные пользователя из URL находится здесь: https://github.com/profcomff/app-template/blob/1070d4370d37529702d7499baeaf145ba4cd9e62/frontend/src/store/profileStore.ts#L15-L28
> ⚠️ Важно: токен в .env.development не коммитим — он только для локального девелопмента.

Метод создает сессию через API с заданными скоупами. Для создания сессий с другим набором скоупов можно писать аналогичные методы.

- `./src/api/user/AuthApi.ts` и `./src/api/user/UserdataApi.ts`
### Интеграция с "Твой ФФ"

в этих файлах хранится код взаимодействия с [Auth API](https://api.profcomff.com/?urls.primaryName=auth)
и [Userdata API](https://api.profcomff.com/?urls.primaryName=userdata), позволяющие получить
информацию о текущем пользователе.
Когда приложение подключено к Твой ФФ, оно ожидает увидеть токен, скоупы и всю дополнительную информацию в параметрах URL. Для этого в `src/store/profileStore.ts` есть метод `fromUrl` -- его стоит вызывать всегда кроме режима разработки.

- По умолчанию используется тестовая среда для общения с API Твой ФФ! Данное поведение меняется в файле `.env`: https://github.com/profcomff/app-template/blob/main/frontend/.env
### Пулл реквесты

Перед пулл-реквестом:

## Инструкции
### Получение кода на свой компьютер
Для работы с данным примером необходимо забрать его к себе на ПК. Для этого нужно:
1. *(Опционально)* Если вы хотите далее опубликовать код на GitHub полезно сначала скопировать
репозиторий к себе кнопкой Fork на GitHub. Кнопка доступна в правом верхнем углу
[страницы репозитория](https://github.com/profcomff/app-template).
2. Создайте папку в удобном вам расположении.
- *Удобно создать папку на рабочем столе с названием вашего приложения.*
3. Откройте приложение "Командная строка" или "Терминал", в зависимости от операционной системы.
4. Прейдите в папку проекта командой `cd /путь/к/папке`.
- Если вы пользователь windows и создали на рабочем столе папку `my_app`, то команда будет
выглядеть так: `cd %userprofile%/Desktop/my_app`
- Если вы пользователь linux или MacOS и создали на рабочем столе папку `my_app`, то команда
будет выглядеть сделующим образом: `cd ~/Desktop/my_app`
5. Склонируйте код репозитория к себе на ПК командой
`git clone https://github.com/profcomff/app-template.git .` (точка в конце означает скачивание
кода в текущую папку). Если вы выполнили пункт 1 используйте в команде ссылку из зеленой кнопки
"Code" в правом верхнем углу вашего репозитория.
6. Откройте код в удобной среде разработки. Рекомендуем использовать VSCode, который можно открыть
из терминала командой `code .`
1. Проверь стили: `pnpm check`
2. _Опционально_: подробная версия `pnpm check:hard`
3. Заполни шаблон пулл-реквеста: что, как и зачем сделано.
4. Запроси ревью у команды.

### Сборка и запуск приложения для публикации
Сборка исходного кода в один пакет производится с помощью Docker. В этом случае создается
независимый от операционной системы пакет, который можно без проблем разместить на любом сервере.
## Сборка и деплой

Выполните команду `make` для сборки приложения. После окончания выполнения этой команды будет создан
новый Docker образ с названием `my_app`, который можно запустить командой `make run`
### Сборка и запуск через Docker:

```bash
make # создаёт Docker образ
make run # запускает приложение
```

### Автосборка
Коммит в main запускает автоматическую сборку проекта средствами GitHub Actions. Настройки автосборки находятся в
файле [.github/workflows/build_and_publish.yml](.github/workflows/build_and_publish.yml).
Dockerfile и docker-compose уже настроены для простого деплоя. В .env указываем базовый URL API:

### Тесты на Pull Request
При создании запроса на слияние, автоматически создаются проверки кода юниттестами и на стили.
Следующие тесты будут запущены:
- Проверки стилей `eslint`, `prettier`, `stylelint` на код в папке `frontend`
```env
VITE_API_BASE_URL=https://api.example.com
```

Настройки автотестов находятся в файле[.github/workflows/checks.yml](.github/workflows/checks.yml).
### CI/CD

### Публикация готового приложения
На данном этапе вам необходимо разместить приложение на каком-либо хостинге, поддерживающем протокол https.
GitHub Actions автоматически проверяет код на PR: eslint, prettier, stylelint

Для этого подойдет любой VPS сервер, который вы можете найти в интернете. Мы рекомендуем использовать VPS сервера на операционной системе linux, для удобства размещения приложений и компонентов использовать Docker.
Автосборка: коммит в main создаёт Docker-образ через workflow .github/workflows/build_and_publish.yml

Чтобы получить SSL сертификаы для поддержки https можно использовать letsencrypt.com, предоставляющий SSL сертификаты бесплатно. Для удобства работы можно использовать веб сервер с встроенной поддержкой этих сертификатов, например Traefik или Caddy.
### Ссылки

Шаблон приложения имеет уже готовые [Dockerfile](cicd/Dockerfile) для сборки вашего приложения, файл [docker-compose.production.yml](cicd/docker-compose.production.yml) с настройками развертывания приложения с Caddy Server и включенным https, а также базой данных PostgreSQL.
API документация -- [Swagger](https://api.test.profcomff.com/?urls.primaryName=rental)
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
"docker-build": "vue-tsc && vite build --base=/ui",
"preview": "vite preview",
"lint": "eslint src/",
"lint:hard": "pnpm lint && pnpm lint:deadcode && pnpm lint:circular",
"lint:deadcode": "knip --exclude binaries,dependencies,unlisted",
"lint:circular": "dpdm --exit-code circular:1 --no-tree --no-warning --progress false --transform ./src/main.ts",
"format": "eslint src/ --fix && prettier src/ --write",
"stylelint": "stylelint 'src/**/*.{vue,css}' --fix",
"check": "vue-tsc && pnpm run format && pnpm run lint && pnpm run stylelint",
"check": "vue-tsc && pnpm lint && pnpm format && pnpm stylelint",
"check:hard": "vue-tsc && pnpm lint:hard && pnpm format && pnpm stylelint",
"test": "echo ok"
},
"dependencies": {
Expand All @@ -25,7 +27,7 @@
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.22.0",
"@mdi/font": "^7.4.47",
"@profcomff/api-uilib": "^2025.10.15",
"@profcomff/api-uilib": "^2025.11.1-test",
"@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
Expand Down Expand Up @@ -56,4 +58,4 @@
"vue-tsc": "^2.2.8",
"vuetify": "^3.7.16"
}
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { ToastType } from './models';
const profileStore = useProfileStore();
const toastStore = useToastStore();

onMounted(() => {
onMounted(async () => {
profileStore.clearLocalStorage();
profileStore.fromUrl();
if (import.meta.env.MODE === 'development') await profileStore.setupDevAdminSession(null);
if (!profileStore.isLoggedIn) {
toastStore.push({
title: 'Не получится оставить отзыв',
Expand Down
8 changes: 4 additions & 4 deletions src/components/TheLecturerSearchTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
{{ item.raw.comments?.length || '—' }}
</template>

<template #[`item.mark_general`]="{ item }">
<v-avatar size="30" :color="getMarkColor(item.raw.mark_general)" class="white--text">
{{ formatMark(item.raw.mark_general) }}
<template #[`item.mark_weighted`]="{ item }">
<v-avatar size="30" :color="getMarkColor(item.raw.mark_weighted)" class="white--text">
{{ formatMark(item.raw.mark_weighted) }}
</v-avatar>
</template>
</v-data-table>
Expand Down Expand Up @@ -80,7 +80,7 @@ const headers: CustomDataTableHeader[] = [
{ title: 'ФИО', key: 'fullName', sortable: false },
{ title: 'Предметы', key: 'subjects', sortable: false },
{ title: 'Отзывы', key: 'comments', align: 'center', sortable: false },
{ title: 'Оценка', key: 'mark_general', align: 'center', sortable: false },
{ title: 'Оценка', key: 'mark_weighted', align: 'center', sortable: false },
];

const tableItems = computed(() => {
Expand Down
1 change: 0 additions & 1 deletion src/components/TheSearchBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ function changeAscDesc() {
emits('changed-asc-desc');
}


async function shareSearch() {
/* eslint-disable @typescript-eslint/no-explicit-any */
const params: Record<string, any> = {};
Expand Down
76 changes: 38 additions & 38 deletions src/pages/AdminPage.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue';
import apiClient from '@/api';
import { Comment } from '@/models';
import AdminReviewCard from '@/components/AdminReviewCard.vue';

const comments = ref<Comment | undefined>();
await loadComments(0);

async function loadComments(offset: number) {
const res = await apiClient.GET('/rating/comment', {
params: {
query: {
limit: 10,
offset,
order_by: ['create_ts'],
unreviewed: true,
},
},
});
console.log(res);
comments.value = res.data;
}
</script>

<template>
<v-container>
<v-btn class="mb-4" color="primary" text="На главную" @click="$router.push('/')"></v-btn>
<v-data-iterator v-if="comments && comments?.comments.length > 0" :items="comments.comments">
<template #default="{ items }">
<template v-for="(item, i) in items" :key="i">
<AdminReviewCard :comment="item" @decided="loadComments" />
</template>
</template>
</v-data-iterator>
<div v-else>Нет комментариев</div>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import apiClient from '@/api';
import { Comment } from '@/models';
import AdminReviewCard from '@/components/AdminReviewCard.vue';
const comments = ref<Comment | undefined>();
await loadComments(0);
async function loadComments(offset: number) {
const res = await apiClient.GET('/rating/comment', {
params: {
query: {
limit: 10,
offset,
order_by: 'create_ts',
unreviewed: true,
},
},
});
console.log(res);
comments.value = res.data;
}
</script>
<template>
<v-container>
<v-btn class="mb-4" color="primary" text="На главную" @click="$router.push('/')"></v-btn>
<v-data-iterator v-if="comments && comments?.comments.length > 0" :items="comments.comments">
<template #default="{ items }">
<template v-for="(item, i) in items" :key="i">
<AdminReviewCard :comment="item" @decided="loadComments" />
</template>
</template>
</v-data-iterator>
<div v-else>Нет комментариев</div>
</v-container>
</template>
Loading