сделал админку конкурсов, сделал шаблон для конкурсов и роутер с главной страницы, подправил выход у админки
This commit is contained in:
parent
203f6763d6
commit
c7533fe87c
@ -3,14 +3,14 @@ from typing import Optional
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.domain.models import Contest
|
from app.domain.models import ContestStatus
|
||||||
|
|
||||||
|
|
||||||
class ContestStatusesRepository:
|
class ContestStatusesRepository:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
async def get_by_id(self, contest_status_id: int) -> Optional[Contest]:
|
async def get_by_id(self, contest_status_id: int) -> Optional[ContestStatus]:
|
||||||
stmt = select(Contest).filter_by(id=contest_status_id)
|
stmt = select(ContestStatus).filter_by(id=contest_status_id)
|
||||||
result = await self.db.execute(stmt)
|
result = await self.db.execute(stmt)
|
||||||
return result.scalars().first()
|
return result.scalars().first()
|
||||||
|
|||||||
@ -69,20 +69,3 @@ async def delete_contest(
|
|||||||
return await contests_service.delete(contest_id, user)
|
return await contests_service.delete(contest_id, user)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'/project/{project_id}/',
|
|
||||||
response_model=Optional[ContestEntity],
|
|
||||||
summary='Get project by contest ID',
|
|
||||||
description='Retrieve project data by contest ID',
|
|
||||||
)
|
|
||||||
async def get_project_by_contest_id(
|
|
||||||
project_id: int,
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
user=Depends(get_current_user),
|
|
||||||
):
|
|
||||||
contests_service = ContestsService(db)
|
|
||||||
contest = await contests_service.get_by_project(project_id)
|
|
||||||
|
|
||||||
return contest
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from typing import Optional
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class ContestEntity(BaseModel):
|
class ContestEntity(BaseModel):
|
||||||
id: int
|
id: Optional[int] = None
|
||||||
title: str
|
title: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
web_url: str
|
web_url: str
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class ContestsService:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.contests_repository = ContestsRepository(db)
|
self.contests_repository = ContestsRepository(db)
|
||||||
self.projects_repository = ProjectsRepository(db)
|
self.projects_repository = ProjectsRepository(db)
|
||||||
self.statuses_repository = ContestStatusesRepository(db)
|
self.contest_statuses_repository = ContestStatusesRepository(db)
|
||||||
self.users_repository = UsersRepository(db)
|
self.users_repository = UsersRepository(db)
|
||||||
|
|
||||||
async def get_all_contests(self) -> list[ContestEntity]:
|
async def get_all_contests(self) -> list[ContestEntity]:
|
||||||
@ -33,7 +33,7 @@ class ContestsService:
|
|||||||
detail='The project with this ID was not found',
|
detail='The project with this ID was not found',
|
||||||
)
|
)
|
||||||
|
|
||||||
status_contest = await self.statuses_repository.get_by_id(contest.status_id)
|
status_contest = await self.contest_statuses_repository.get_by_id(contest.status_id)
|
||||||
if status_contest is None:
|
if status_contest is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -75,7 +75,7 @@ class ContestsService:
|
|||||||
detail='The project with this ID was not found',
|
detail='The project with this ID was not found',
|
||||||
)
|
)
|
||||||
|
|
||||||
status_contest = await self.statuses_repository.get_by_id(contest.role_id)
|
status_contest = await self.contest_statuses_repository.get_by_id(contest.status_id)
|
||||||
if status_contest is None:
|
if status_contest is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -114,17 +114,6 @@ class ContestsService:
|
|||||||
|
|
||||||
return self.model_to_entity(result)
|
return self.model_to_entity(result)
|
||||||
|
|
||||||
async def get_by_project(self, project_id: int) -> Optional[ContestEntity]:
|
|
||||||
project = await self.projects_repository.get_by_id(project_id)
|
|
||||||
if project is None:
|
|
||||||
raise HTTPException(status_code=404, detail='Project not found')
|
|
||||||
|
|
||||||
contest_model = await self.contests_repository.get_by_id(project.contest_id)
|
|
||||||
if not contest_model:
|
|
||||||
raise HTTPException(status_code=404, detail='Contest not found')
|
|
||||||
|
|
||||||
return self.model_to_entity(contest_model)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def model_to_entity(contest_model: Contest) -> ContestEntity:
|
def model_to_entity(contest_model: Contest) -> ContestEntity:
|
||||||
return ContestEntity(
|
return ContestEntity(
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const createProfile = async (profile) => {
|
const createContest = async (contest) => {
|
||||||
console.log(profile)
|
console.log(contest)
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${CONFIG.BASE_URL}/profiles`,
|
`${CONFIG.BASE_URL}/contests/`,
|
||||||
profile,
|
contest,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
@ -21,4 +21,4 @@ const createProfile = async (profile) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createProfile
|
export default createContest
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const deleteProfile = async (profileId) => {
|
const deleteContest = async (contestId) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // получение токена
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
const response = await axios.delete(
|
const response = await axios.delete(
|
||||||
`${CONFIG.BASE_URL}/profiles/${profileId}`,
|
`${CONFIG.BASE_URL}/contests/${contestId}`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
@ -19,4 +19,4 @@ const deleteProfile = async (profileId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default deleteProfile
|
export default deleteContest
|
||||||
@ -3,7 +3,7 @@ import CONFIG from "@/core/config.js";
|
|||||||
|
|
||||||
const fetchContests = async () => {
|
const fetchContests = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/contests`, {
|
const response = await axios.get(`${CONFIG.BASE_URL}/contests/`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import CONFIG from '../../core/config.js'
|
|
||||||
|
|
||||||
|
|
||||||
const fetchProfile = async () => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem("access_token");
|
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/profiles`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
throw new Error("Нет доступа к пользователям");
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
throw new Error("Доступ запрещён");
|
|
||||||
}
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default fetchProfile;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const updateProfile = async (profile) => {
|
const updateContest = async (profile) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ const updateProfile = async (profile) => {
|
|||||||
console.log('Отправляем на сервер:', profileData)
|
console.log('Отправляем на сервер:', profileData)
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/profiles/${id}`,
|
`${CONFIG.BASE_URL}/contests/${id}/`,
|
||||||
profileData,
|
profileData,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -28,4 +28,4 @@ const updateProfile = async (profile) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateProfile
|
export default updateContest
|
||||||
@ -2,11 +2,10 @@ import axios from 'axios'
|
|||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const createProfile = async (profile) => {
|
const createProfile = async (profile) => {
|
||||||
console.log(profile)
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${CONFIG.BASE_URL}/profiles`,
|
`${CONFIG.BASE_URL}/profiles/`,
|
||||||
profile,
|
profile,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const deleteProfile = async (profileId) => {
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // получение токена
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
const response = await axios.delete(
|
const response = await axios.delete(
|
||||||
`${CONFIG.BASE_URL}/profiles/${profileId}`,
|
`${CONFIG.BASE_URL}/profiles/${profileId}/`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import CONFIG from '../../core/config.js'
|
|||||||
const fetchProfile = async () => {
|
const fetchProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/profiles`, {
|
const response = await axios.get(`${CONFIG.BASE_URL}/profiles/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,10 +8,8 @@ const updateProfile = async (profile) => {
|
|||||||
// Убираем id из тела запроса, он идет в URL
|
// Убираем id из тела запроса, он идет в URL
|
||||||
const { id, ...profileData } = profile
|
const { id, ...profileData } = profile
|
||||||
|
|
||||||
console.log('Отправляем на сервер:', profileData)
|
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/profiles/${id}`,
|
`${CONFIG.BASE_URL}/profiles/${id}/`,
|
||||||
profileData,
|
profileData,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@ -21,7 +19,6 @@ const updateProfile = async (profile) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('Ответ от сервера:', response.data)
|
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.response?.data?.detail || error.message)
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import axios from 'axios'
|
|||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
const createProject = async (project) => {
|
const createProject = async (project) => {
|
||||||
console.log(project)
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
const token = localStorage.getItem('access_token') // или другой способ получения токена
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${CONFIG.BASE_URL}/projects`,
|
`${CONFIG.BASE_URL}/projects/`,
|
||||||
project,
|
project,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const deleteProject = async (projectId) => {
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // получение токена
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
const response = await axios.delete(
|
const response = await axios.delete(
|
||||||
`${CONFIG.BASE_URL}/projects/${projectId}`,
|
`${CONFIG.BASE_URL}/projects/${projectId}/`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const fetchProjects = async () => {
|
|||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/projects`, {
|
const response = await axios.get(`${CONFIG.BASE_URL}/projects`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}/`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const updateProject = async (project) => {
|
|||||||
|
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/projects/${id}`,
|
`${CONFIG.BASE_URL}/projects/${id}/`,
|
||||||
projectData,
|
projectData,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const createTeam = async (team) => {
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // или другой способ
|
const token = localStorage.getItem('access_token') // или другой способ
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${CONFIG.BASE_URL}/teams`,
|
`${CONFIG.BASE_URL}/teams/`,
|
||||||
team,
|
team,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const deleteTeam = async (teamId) => {
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') // получение токена
|
const token = localStorage.getItem('access_token') // получение токена
|
||||||
const response = await axios.delete(
|
const response = await axios.delete(
|
||||||
`${CONFIG.BASE_URL}/teams/${teamId}`,
|
`${CONFIG.BASE_URL}/teams/${teamId}/`,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import CONFIG from "@/core/config.js";
|
|||||||
const fetchTeams = async () => {
|
const fetchTeams = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
const response = await axios.get(`${CONFIG.BASE_URL}/teams`, {
|
const response = await axios.get(`${CONFIG.BASE_URL}/teams/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const updateTeam = async (team) => {
|
|||||||
|
|
||||||
|
|
||||||
const response = await axios.put(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/teams/${id}`,
|
`${CONFIG.BASE_URL}/teams/${id}/`,
|
||||||
teamData,
|
teamData,
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@ -30,7 +30,15 @@ import {
|
|||||||
QTooltip,
|
QTooltip,
|
||||||
QBanner,
|
QBanner,
|
||||||
QSlideTransition,
|
QSlideTransition,
|
||||||
Ripple
|
Ripple,
|
||||||
|
QToggle,
|
||||||
|
QList,
|
||||||
|
QSpinnerDots,
|
||||||
|
QCarouselSlide,
|
||||||
|
QCarousel,
|
||||||
|
QItemSection,
|
||||||
|
QItemLabel,
|
||||||
|
QItem,
|
||||||
} from 'quasar'
|
} from 'quasar'
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +56,9 @@ app.use(Quasar, {
|
|||||||
QLayout, QPageContainer, QPage,
|
QLayout, QPageContainer, QPage,
|
||||||
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
|
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
|
||||||
QSeparator, QCardActions, QDialog, QIcon, QSpace,
|
QSeparator, QCardActions, QDialog, QIcon, QSpace,
|
||||||
QAvatar, QTooltip, QBanner, QSlideTransition
|
QAvatar, QTooltip, QBanner, QSlideTransition, QToggle,
|
||||||
|
QList, QSpinnerDots, QCarouselSlide, QCarousel,
|
||||||
|
QItemSection, QItemLabel, QItem
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
Ripple
|
Ripple
|
||||||
|
|||||||
@ -175,9 +175,12 @@
|
|||||||
<template v-else-if="dialogType === 'contests'">
|
<template v-else-if="dialogType === 'contests'">
|
||||||
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
|
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
|
||||||
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.start_date" label="Дата начала" dense clearable class="q-mt-sm" type="date" />
|
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.end_date" label="Дата окончания" dense clearable class="q-mt-sm" type="date" />
|
<q-input v-model="dialogData.photo" label="Фото" dense clearable class="q-mt-sm" />
|
||||||
<!-- Другие поля конкурса -->
|
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
||||||
|
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
||||||
|
<q-input v-model="dialogData.project_id" label="Проект" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -187,28 +190,28 @@
|
|||||||
|
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="dialogType === 'teams'"
|
v-if="dialogType === 'teams' && dialogData.id"
|
||||||
flat
|
flat
|
||||||
label="Удалить"
|
label="Удалить"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="deleteItem"
|
@click="deleteItem"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="dialogType === 'profiles'"
|
v-if="dialogType === 'profiles' && dialogData.id"
|
||||||
flat
|
flat
|
||||||
label="Удалить"
|
label="Удалить"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="deleteItem"
|
@click="deleteItem"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="dialogType === 'projects'"
|
v-if="dialogType === 'projects' && dialogData.id"
|
||||||
flat
|
flat
|
||||||
label="Удалить"
|
label="Удалить"
|
||||||
color="negative"
|
color="negative"
|
||||||
@click="deleteItem"
|
@click="deleteItem"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="dialogType === 'contests'"
|
v-if="dialogType === 'contests' && dialogData.id"
|
||||||
flat
|
flat
|
||||||
label="Удалить"
|
label="Удалить"
|
||||||
color="negative"
|
color="negative"
|
||||||
@ -226,6 +229,16 @@
|
|||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- Кнопка выхода / авторизации -->
|
||||||
|
<q-btn
|
||||||
|
:icon="'logout'"
|
||||||
|
class="fixed-bottom-right q-ma-md"
|
||||||
|
size="20px"
|
||||||
|
color="indigo-10"
|
||||||
|
round
|
||||||
|
@click="handleAuthAction"
|
||||||
|
/>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -248,10 +261,12 @@ import updateProfile from '@/api/profiles/updateProfile.js'
|
|||||||
import deleteProfileById from '@/api/profiles/deleteProfile.js'
|
import deleteProfileById from '@/api/profiles/deleteProfile.js'
|
||||||
import createProfile from '@/api/profiles/createProfile.js'
|
import createProfile from '@/api/profiles/createProfile.js'
|
||||||
|
|
||||||
// import fetchContests from '@/api/contests/getContests.js'
|
import fetchContests from '@/api/contests/getContests.js'
|
||||||
// import updateContest from '@/api/contests/updateContest.js'
|
import updateContest from '@/api/contests/updateContest.js'
|
||||||
// import deleteContestById from '@/api/contests/deleteContest.js'
|
import deleteContestById from '@/api/contests/deleteContest.js'
|
||||||
// import createContest from '@/api/contests/createContest.js'
|
import createContest from '@/api/contests/createContest.js'
|
||||||
|
import {Notify} from "quasar";
|
||||||
|
import router from "@/router/index.js";
|
||||||
|
|
||||||
// Текущая вкладка — 'teams' или 'projects'
|
// Текущая вкладка — 'teams' или 'projects'
|
||||||
const tab = ref('profiles')
|
const tab = ref('profiles')
|
||||||
@ -295,11 +310,14 @@ const loadingContests = ref(false)
|
|||||||
const contestColumns = [
|
const contestColumns = [
|
||||||
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
||||||
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||||
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
{ name: 'web_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
||||||
|
{ name: 'photo', label: 'Фото', field: 'photo', sortable: true },
|
||||||
|
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
||||||
|
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
||||||
|
{ name: 'project_id', label: 'Проект', field: 'project_id', sortable: true },
|
||||||
|
{ name: 'status_id', label: 'Статус', field: 'status_id', sortable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Общие состояния для диалогов
|
// Общие состояния для диалогов
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogData = ref({})
|
const dialogData = ref({})
|
||||||
@ -356,6 +374,15 @@ async function saveChanges() {
|
|||||||
const newProfile = await createProfile(dialogData.value)
|
const newProfile = await createProfile(dialogData.value)
|
||||||
profiles.value.push(newProfile)
|
profiles.value.push(newProfile)
|
||||||
}
|
}
|
||||||
|
} else if (dialogType.value === 'contests') {
|
||||||
|
if (dialogData.value.id) {
|
||||||
|
await updateContest(dialogData.value)
|
||||||
|
const idx = contests.value.findIndex(c => c.id === dialogData.value.id)
|
||||||
|
if (idx !== -1) contests.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
||||||
|
} else {
|
||||||
|
const newContest = await createContest(dialogData.value)
|
||||||
|
profiles.value.push(newContest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -394,6 +421,16 @@ async function loadData(name) {
|
|||||||
} finally {
|
} finally {
|
||||||
loadingProfiles.value = false
|
loadingProfiles.value = false
|
||||||
}
|
}
|
||||||
|
} else if (name === 'contests') {
|
||||||
|
loadingContests.value = true
|
||||||
|
try {
|
||||||
|
contests.value = await fetchContests() || []
|
||||||
|
} catch (error) {
|
||||||
|
contests.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingContests.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,6 +446,9 @@ async function deleteItem() {
|
|||||||
} else if (dialogType.value === 'profiles') {
|
} else if (dialogType.value === 'profiles') {
|
||||||
await deleteProfileById(dialogData.value.id)
|
await deleteProfileById(dialogData.value.id)
|
||||||
profiles.value = profiles.value.filter(p => p.id !== dialogData.value.id)
|
profiles.value = profiles.value.filter(p => p.id !== dialogData.value.id)
|
||||||
|
} else if (dialogType.value === 'contests') {
|
||||||
|
await deleteContestById(dialogData.value.id)
|
||||||
|
contests.value = contests.value.filter(c => c.id !== dialogData.value.id)
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -417,6 +457,7 @@ async function deleteItem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createHandler() {
|
function createHandler() {
|
||||||
|
dialogData.value = {};
|
||||||
openEdit(tab.value, null)
|
openEdit(tab.value, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,6 +476,24 @@ onMounted(() => {
|
|||||||
watch(tab, (newTab) => {
|
watch(tab, (newTab) => {
|
||||||
loadData(newTab)
|
loadData(newTab)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleAuthAction = () => {
|
||||||
|
const isAuthenticated = ref(!!localStorage.getItem('access_token'))
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('user_id')
|
||||||
|
isAuthenticated.value = false
|
||||||
|
|
||||||
|
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Выход успешно осуществлен',
|
||||||
|
icon: 'check_circle',
|
||||||
|
})
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
299
WEB/src/pages/ContestDetailPage.vue
Normal file
299
WEB/src/pages/ContestDetailPage.vue
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<template>
|
||||||
|
<q-page class="contest-detail-page bg-violet-strong q-pa-md">
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
icon="arrow_back"
|
||||||
|
label="Обратно на страницу"
|
||||||
|
flat
|
||||||
|
color="white"
|
||||||
|
@click="router.back()"
|
||||||
|
class="q-mb-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex flex-center q-pt-xl" style="min-height: 50vh;">
|
||||||
|
<q-spinner-dots color="white" size="5em" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="contest" class="q-gutter-y-xl">
|
||||||
|
<div class="flex justify-center q-mb-md">
|
||||||
|
<q-avatar size="140px" class="contest-logo shadow-12">
|
||||||
|
<img :src="contest.photo || 'https://cdn.quasar.dev/logo-v2/svg/logo.svg'" alt="Логотип конкурса"/>
|
||||||
|
</q-avatar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center q-mb-xl">
|
||||||
|
<q-card class="contest-name-card">
|
||||||
|
<q-card-section class="text-h4 text-center text-indigo-10 q-pa-md">
|
||||||
|
{{ contest.title }}
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center q-mb-xl">
|
||||||
|
<q-card class="description-card violet-card" style="max-width: 940px; width: 100%;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6 text-indigo-10 q-mb-sm">Описание</div>
|
||||||
|
<div class="text-body1 text-indigo-9 q-mb-md">{{ contest.description }}</div>
|
||||||
|
|
||||||
|
<div v-if="contest.web_url" class="q-mt-md text-indigo-9">
|
||||||
|
<q-icon name="link" size="sm" class="q-mr-xs" />
|
||||||
|
<a :href="contest.web_url" target="_blank" class="text-indigo-9" style="text-decoration: none;">
|
||||||
|
Перейти на сайт конкурса
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="contest.results" class="q-mt-md text-indigo-9">
|
||||||
|
<q-icon name="emoji_events" size="sm" class="q-mr-xs" />
|
||||||
|
Результаты: <span class="text-weight-bold">{{ contest.results }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="contest.is_win" class="q-mt-md text-indigo-9">
|
||||||
|
<q-icon name="celebration" size="sm" class="q-mr-xs" />
|
||||||
|
<span class="text-weight-bold text-positive">Победа!</span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="contest.carousel_photos && contest.carousel_photos.length > 0" class="flex justify-center q-mb-xl">
|
||||||
|
<q-card class="violet-card" style="max-width: 940px; width: 100%;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6 text-indigo-10 q-mb-md">Галерея</div>
|
||||||
|
<q-carousel
|
||||||
|
v-model="slide"
|
||||||
|
transition-prev="slide-right"
|
||||||
|
transition-next="slide-left"
|
||||||
|
swipeable
|
||||||
|
animated
|
||||||
|
control-color="indigo-10"
|
||||||
|
navigation
|
||||||
|
arrows
|
||||||
|
autoplay
|
||||||
|
infinite
|
||||||
|
class="rounded-borders"
|
||||||
|
height="300px"
|
||||||
|
>
|
||||||
|
<q-carousel-slide v-for="(photo, index) in contest.carousel_photos" :key="index" :name="index + 1" :img-src="photo.url" />
|
||||||
|
</q-carousel>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="contest.files && contest.files.length > 0" class="flex justify-center q-mb-xl">
|
||||||
|
<q-card class="violet-card" style="max-width: 940px; width: 100%;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6 text-indigo-10 q-mb-md">Файлы</div>
|
||||||
|
<q-list separator bordered class="rounded-borders">
|
||||||
|
<q-item v-for="file in contest.files" :key="file.id" clickable v-ripple :href="file.url" target="_blank">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="folder_open" color="indigo-8" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ file.name }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ file.description }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="download" color="indigo-6" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="contestParticipants.length > 0" class="flex justify-center flex-wrap q-gutter-md q-mb-xl">
|
||||||
|
<div class="flex justify-center q-mb-md" style="width: 100%;">
|
||||||
|
<q-card class="contest-name-card"> <q-card-section class="text-h6 text-center text-indigo-10 q-pa-md">
|
||||||
|
Участники конкурса
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<q-card
|
||||||
|
v-for="member in contestParticipants"
|
||||||
|
:key="member.id"
|
||||||
|
class="member-card violet-card"
|
||||||
|
bordered
|
||||||
|
style="width: 180px;"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-card-section class="q-pa-md flex flex-center">
|
||||||
|
<q-avatar size="64px" class="shadow-6">
|
||||||
|
<img :src="member.avatar" :alt="member.name"/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<div class="text-subtitle1 text-center text-indigo-11">{{ member.name }}</div>
|
||||||
|
<div class="text-caption text-center text-indigo-9">{{ member.role }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-separator class="q-my-lg" color="indigo-4" style="width: 80%; margin: 0 auto;"/>
|
||||||
|
<div class="q-mt-md"></div>
|
||||||
|
|
||||||
|
<div v-if="contest.repository_url" class="flex justify-center q-mb-xl">
|
||||||
|
<q-card class="violet-card" style="max-width: 940px; width: 100%;">
|
||||||
|
<q-card-section class="q-pa-md">
|
||||||
|
<div class="text-h6 text-indigo-10 q-mb-md">Репозиторий решения</div>
|
||||||
|
<div class="text-body1 text-indigo-9">
|
||||||
|
<q-icon name="code" size="sm" class="q-mr-xs" />
|
||||||
|
<a :href="contest.repository_url" target="_blank" class="text-indigo-9" style="text-decoration: none; word-break: break-all;">
|
||||||
|
{{ contest.repository_url }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-center q-pt-xl text-white text-h5" style="min-height: 50vh;">
|
||||||
|
Конкурс не найден :(
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { Ripple, Notify } from 'quasar';
|
||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
// Директивы
|
||||||
|
defineExpose({ directives: { ripple: Ripple } });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// --- Состояние конкурса ---
|
||||||
|
const contest = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const contestId = computed(() => route.params.id);
|
||||||
|
|
||||||
|
// --- Карусель фото ---
|
||||||
|
const slide = ref(1);
|
||||||
|
|
||||||
|
// --- Участники конкурса (моковые данные) ---
|
||||||
|
const contestParticipants = ref([
|
||||||
|
{ id: 1, name: 'Иван Иванов', role: 'Team Lead', avatar: 'https://randomuser.me/api/portraits/men/32.jpg' },
|
||||||
|
{ id: 2, name: 'Мария Петрова', role: 'Frontend', avatar: 'https://randomuser.me/api/portraits/women/44.jpg' },
|
||||||
|
{ id: 3, name: 'Алексей Смирнов', role: 'Backend', avatar: 'https://randomuser.me/api/portraits/men/65.jpg' },
|
||||||
|
// Добавьте больше участников или динамически загружайте их
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Загрузка данных конкурса ---
|
||||||
|
async function fetchContestDetails(id) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// В реальном приложении здесь будет API-запрос:
|
||||||
|
// const response = await axios.get(`${CONFIG.BASE_URL}/contests/${id}`);
|
||||||
|
// contest.value = response.data;
|
||||||
|
|
||||||
|
// Моковые данные для примера (замените на реальный fetch)
|
||||||
|
const mockContests = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Hackathon 2024',
|
||||||
|
description: 'Ежегодный хакатон для стартапов, где команды соревнуются в создании инновационных решений за короткий период времени. Фокус на Web3 и AI технологиях.',
|
||||||
|
web_url: 'https://example.com/hackathon2024',
|
||||||
|
repository_url: 'https://github.com/my-team/hackathon2024-solution',
|
||||||
|
photo: 'https://cdn.quasar.dev/img/parallax2.jpg',
|
||||||
|
results: '1 место в категории "Лучшее AI-решение"',
|
||||||
|
is_win: true,
|
||||||
|
carousel_photos: [
|
||||||
|
{ id: 1, url: 'https://images.unsplash.com/photo-1668796319088-214d6a82d54b?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||||
|
{ id: 2, url: 'https://cdn.quasar.dev/img/quasar.jpg' },
|
||||||
|
{ id: 3, url: 'https://cdn.quasar.dev/img/parallax1.jpg' },
|
||||||
|
{ id: 4, url: 'https://cdn.quasar.dev/img/donuts.png' },
|
||||||
|
{ id: 5, url: 'https://cdn.quasar.dev/img/parallax2.jpg' },
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
{ id: 1, name: 'Презентация проекта.pdf', description: 'Финальная презентация', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
|
||||||
|
{ id: 2, name: 'Код проекта.zip', description: 'Исходный код', url: 'https://www.learningcontainer.com/wp-content/uploads/2020/07/Example-Zip-File.zip' },
|
||||||
|
],
|
||||||
|
project_id: 1,
|
||||||
|
status_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'CodeFest',
|
||||||
|
description: 'Масштабное соревнование по спортивному программированию, где участники решают алгоритмические задачи. Отличная возможность проверить свои навыки.',
|
||||||
|
web_url: 'https://codefest.org',
|
||||||
|
repository_url: 'https://gitlab.com/awesome-devs/codefest-challenge',
|
||||||
|
photo: 'https://cdn.quasar.dev/img/material.png',
|
||||||
|
results: null,
|
||||||
|
is_win: false,
|
||||||
|
carousel_photos: [
|
||||||
|
{ id: 10, url: 'https://images.unsplash.com/photo-1584291378203-674a462de8bc?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||||
|
{ id: 11, url: 'https://cdn.quasar.dev/img/chicken-salad.jpg' },
|
||||||
|
], // Добавлены фото для второго конкурса
|
||||||
|
files: [],
|
||||||
|
project_id: 2,
|
||||||
|
status_id: 2,
|
||||||
|
},
|
||||||
|
// Добавьте другие моковые конкурсы по мере необходимости
|
||||||
|
];
|
||||||
|
|
||||||
|
contest.value = mockContests.find(c => c.id === parseInt(id));
|
||||||
|
|
||||||
|
if (!contest.value) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Конкурс с таким ID не найден.',
|
||||||
|
icon: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки деталей конкурса:', error);
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Не удалось загрузить информацию о конкурсе.',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
contest.value = null; // Сброс, если ошибка
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchContestDetails(contestId.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(contestId, async (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
await fetchContestDetails(newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.contest-detail-page {
|
||||||
|
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-logo {
|
||||||
|
border: 4px solid #ede9fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-name-card {
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #ede9fe;
|
||||||
|
box-shadow: 0 6px 32px rgba(124, 58, 237, 0.13), 0 1.5px 6px rgba(124, 58, 237, 0.10);
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.violet-card {
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #ede9fe;
|
||||||
|
box-shadow: 0 6px 32px rgba(124, 58, 237, 0.13), 0 1.5px 6px rgba(124, 58, 237, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.member-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -46,9 +46,8 @@
|
|||||||
:key="contest.id"
|
:key="contest.id"
|
||||||
class="contest-card violet-card"
|
class="contest-card violet-card"
|
||||||
bordered
|
bordered
|
||||||
style="width: 220px;"
|
style="width: 220px; cursor: pointer;" v-ripple
|
||||||
v-ripple
|
@click="router.push({ name: 'contest-detail', params: { id: contest.id } })" >
|
||||||
>
|
|
||||||
<q-card-section class="q-pa-md">
|
<q-card-section class="q-pa-md">
|
||||||
<div class="text-h6">{{ contest.title }}</div>
|
<div class="text-h6">{{ contest.title }}</div>
|
||||||
<div class="text-subtitle2 text-indigo-8">{{ contest.description }}</div>
|
<div class="text-subtitle2 text-indigo-8">{{ contest.description }}</div>
|
||||||
@ -241,7 +240,7 @@ function getMonthMargin(idx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка активности из API
|
// Загрузка активности из API
|
||||||
const username = 'Numerum';
|
const username = 'archibald';
|
||||||
|
|
||||||
async function loadActivity() {
|
async function loadActivity() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -2,11 +2,17 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import LoginPage from "../pages/LoginPage.vue"
|
import LoginPage from "../pages/LoginPage.vue"
|
||||||
import HomePage from "../pages/HomePage.vue"
|
import HomePage from "../pages/HomePage.vue"
|
||||||
import AdminPage from "../pages/AdminPage.vue"
|
import AdminPage from "../pages/AdminPage.vue"
|
||||||
|
import ContestDetailPage from "@/pages/ContestDetailPage.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: HomePage },
|
{ path: '/', component: HomePage },
|
||||||
{ path: '/login', component: LoginPage },
|
{ path: '/login', component: LoginPage },
|
||||||
{ path: '/admin', component: AdminPage }
|
{ path: '/admin', component: AdminPage },
|
||||||
|
{
|
||||||
|
path: '/contests/:id', // Динамический маршрут, :id будет ID конкурса
|
||||||
|
name: 'contest-detail', // Имя маршрута для удобства навигации (например, router.push({ name: 'contest-detail', params: { id: 123 } }))
|
||||||
|
component: ContestDetailPage // Компонент, который будет отображаться
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user