From 00262bb69915bbb3b4926ace1e1d9324d9f14f80 Mon Sep 17 00:00:00 2001 From: Doctorixx <61980858+Windows-up@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:36:36 +0300 Subject: [PATCH 1/2] backend: add posts --- .../codebattles/backend/dto/CreatePostDto.kt | 19 +++++ .../ru/codebattles/backend/dto/PostDto.kt | 19 +++++ .../backend/dto/mapper/CreatePostMapper.kt | 9 +++ .../backend/dto/mapper/PostMapper.kt | 9 +++ .../ru/codebattles/backend/entity/Posts.kt | 18 +++++ .../backend/repository/PostRepository.kt | 8 ++ .../backend/services/PostService.kt | 67 +++++++++++++++ .../backend/web/controllers/PostController.kt | 81 +++++++++++++++++++ .../resources/db/migrations/V7__add_posts.sql | 10 +++ 9 files changed, 240 insertions(+) create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreatePostDto.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/PostDto.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreatePostMapper.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/PostMapper.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Posts.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/PostRepository.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/PostService.kt create mode 100644 BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PostController.kt create mode 100644 BACKEND_V2/src/main/resources/db/migrations/V7__add_posts.sql diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreatePostDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreatePostDto.kt new file mode 100644 index 0000000..de4a0e8 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/CreatePostDto.kt @@ -0,0 +1,19 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull + +data class CreatePostDto( + @field:NotBlank + @Schema(description = "Title of the post", example = "Welcome to Code Battles") + val title: String, + + @field:NotBlank + @Schema(description = "Content of the post", example = "This is the main content of the post...") + val content: String, + + @field:NotNull + @Schema(description = "Show post at main page", example = "true") + val showAtMain: Boolean, +) diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/PostDto.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/PostDto.kt new file mode 100644 index 0000000..25bb53b --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/PostDto.kt @@ -0,0 +1,19 @@ +package ru.codebattles.backend.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class PostDto( + @Schema(description = "Unique identifier of the post", example = "1") + val id: Long? = null, + + + @Schema(description = "Title of post") + val title: String, + + @Schema(description = "Content of post") + val content: String, + + @Schema(description = "Show post at main page") + val showAtMain: Boolean, +) + diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreatePostMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreatePostMapper.kt new file mode 100644 index 0000000..bbec60f --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/CreatePostMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.CreatePostDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Posts + +@Mapper(componentModel = "spring") +interface CreatePostMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/PostMapper.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/PostMapper.kt new file mode 100644 index 0000000..4d7021e --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/mapper/PostMapper.kt @@ -0,0 +1,9 @@ +package ru.codebattles.backend.dto.mapper + +import org.mapstruct.Mapper +import ru.codebattles.backend.dto.PostDto +import ru.codebattles.backend.dto.mapper.core.AbstractMapper +import ru.codebattles.backend.entity.Posts + +@Mapper(componentModel = "spring") +interface PostMapper : AbstractMapper diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Posts.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Posts.kt new file mode 100644 index 0000000..6790d5d --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Posts.kt @@ -0,0 +1,18 @@ +package ru.codebattles.backend.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table + +@Entity +@Table(name = "posts") +data class Posts( + @Column(nullable = false) + var title: String, + + @Column(nullable = false) + var content: String, + + @Column(nullable = false) + var showAtMain: Boolean, +) : BaseEntity() diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/PostRepository.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/PostRepository.kt new file mode 100644 index 0000000..d297514 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/repository/PostRepository.kt @@ -0,0 +1,8 @@ +package ru.codebattles.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import ru.codebattles.backend.entity.Posts + +interface PostRepository : JpaRepository { + fun findByShowAtMainTrue(): List +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/PostService.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/PostService.kt new file mode 100644 index 0000000..4960758 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/services/PostService.kt @@ -0,0 +1,67 @@ +package ru.codebattles.backend.services + +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.server.ResponseStatusException +import ru.codebattles.backend.dto.CreatePostDto +import ru.codebattles.backend.dto.PostDto +import ru.codebattles.backend.dto.mapper.PostMapper +import ru.codebattles.backend.entity.Posts +import ru.codebattles.backend.repository.PostRepository + +@Service +class PostService( + private val postRepository: PostRepository, + private val postMapper: PostMapper, +) { + + fun getAll(): List { + return postMapper.toDtoS( + postRepository.findAll() + ) + } + + fun getById(id: Long): PostDto { + val optionalPost = postRepository.findById(id) + if (optionalPost.isPresent) { + return postMapper.toDto(optionalPost.get()) + } + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found") + } + + fun getMainPagePosts(): List { + return postMapper.toDtoS( + postRepository.findByShowAtMainTrue() + ) + } + + fun create(createPostDto: CreatePostDto): PostDto { + val post = Posts( + title = createPostDto.title, + content = createPostDto.content, + showAtMain = createPostDto.showAtMain + ) + + val savedPost = postRepository.save(post) + return postMapper.toDto(savedPost) + } + + fun update(id: Long, postDto: PostDto): PostDto { + val existingPost = postRepository.findById(id) + .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found") } + + existingPost.title = postDto.title + existingPost.content = postDto.content + existingPost.showAtMain = postDto.showAtMain + + val updatedPost = postRepository.save(existingPost) + return postMapper.toDto(updatedPost) + } + + fun delete(id: Long) { + if (!postRepository.existsById(id)) { + throw ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found") + } + postRepository.deleteById(id) + } +} diff --git a/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PostController.kt b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PostController.kt new file mode 100644 index 0000000..d3433d2 --- /dev/null +++ b/BACKEND_V2/src/main/kotlin/ru/codebattles/backend/web/controllers/PostController.kt @@ -0,0 +1,81 @@ +package ru.codebattles.backend.web.controllers + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.annotation.security.RolesAllowed +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import ru.codebattles.backend.dto.CreatePostDto +import ru.codebattles.backend.dto.PostDto +import ru.codebattles.backend.entity.User +import ru.codebattles.backend.services.PostService + +@Tag(name = "Posts", description = "Endpoints for managing posts") +@RestController +@RequestMapping("/api/posts") +@SecurityRequirement(name = "JWT") +class PostController( + private val postService: PostService, +) { + + @Operation( + summary = "Get all posts", + description = "Retrieves a list of all posts." + ) + @GetMapping + fun getAll(): List { + return postService.getAll() + } + + @Operation( + summary = "Get main page posts", + description = "Retrieves posts that should be shown on the main page." + ) + @GetMapping("/main") + fun getMainPagePosts(): List { + return postService.getMainPagePosts() + } + + @Operation( + summary = "Get post by ID", + description = "Retrieves a post by its ID." + ) + @GetMapping("/{id}") + fun getById(@PathVariable id: Long): PostDto { + return postService.getById(id) + } + + @Operation( + summary = "[ADMIN] Create a new post", + description = "Creates a new post. Required admin role." + ) + @RolesAllowed("ADMIN") + @PostMapping + fun create(@RequestBody createPostDto: CreatePostDto, @AuthenticationPrincipal user: User): PostDto { + return postService.create(createPostDto) + } + + @Operation( + summary = "[ADMIN] Update a post", + description = "Updates an existing post by ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @PutMapping("/{id}") + fun update(@PathVariable id: Long, @RequestBody postDto: PostDto, @AuthenticationPrincipal user: User): PostDto { + return postService.update(id, postDto) + } + + @Operation( + summary = "[ADMIN] Delete a post", + description = "Deletes a post by ID. Required admin role." + ) + @RolesAllowed("ADMIN") + @DeleteMapping("/{id}") + fun delete(@PathVariable id: Long, @AuthenticationPrincipal user: User): ResponseEntity { + postService.delete(id) + return ResponseEntity.noContent().build() + } +} diff --git a/BACKEND_V2/src/main/resources/db/migrations/V7__add_posts.sql b/BACKEND_V2/src/main/resources/db/migrations/V7__add_posts.sql new file mode 100644 index 0000000..2c9b683 --- /dev/null +++ b/BACKEND_V2/src/main/resources/db/migrations/V7__add_posts.sql @@ -0,0 +1,10 @@ +CREATE TABLE posts +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + title VARCHAR(255) NOT NULL, + content VARCHAR(255) NOT NULL, + show_at_main BOOLEAN NOT NULL, + CONSTRAINT pk_posts PRIMARY KEY (id) +); \ No newline at end of file From 3e3b5e3f2e56792d2bac00d40da7014f7c09d33d Mon Sep 17 00:00:00 2001 From: Doctorixx <61980858+Windows-up@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:52:09 +0300 Subject: [PATCH 2/2] frontend: add posts --- FRONTEND_V2/src/App.jsx | 13 ++++ FRONTEND_V2/src/components/AdminHeader.jsx | 1 + .../src/components/form_impl/PostForm.jsx | 35 +++++++++ FRONTEND_V2/src/locales/en.json | 18 ++++- FRONTEND_V2/src/locales/ru.json | 19 ++++- .../src/pages/admin/pages/AdminPostPage.jsx | 72 +++++++++++++++++++ .../admin/pages/AdminPostsPageCreate.jsx | 57 +++++++++++++++ .../pages/admin/pages/AdminPostsPageEdit.jsx | 66 +++++++++++++++++ FRONTEND_V2/src/pages/shared/PostPage.jsx | 33 +++++++++ FRONTEND_V2/src/pages/user/ChampsPage.jsx | 26 +++++++ 10 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 FRONTEND_V2/src/components/form_impl/PostForm.jsx create mode 100644 FRONTEND_V2/src/pages/admin/pages/AdminPostPage.jsx create mode 100644 FRONTEND_V2/src/pages/admin/pages/AdminPostsPageCreate.jsx create mode 100644 FRONTEND_V2/src/pages/admin/pages/AdminPostsPageEdit.jsx create mode 100644 FRONTEND_V2/src/pages/shared/PostPage.jsx diff --git a/FRONTEND_V2/src/App.jsx b/FRONTEND_V2/src/App.jsx index 892235a..7c2de45 100644 --- a/FRONTEND_V2/src/App.jsx +++ b/FRONTEND_V2/src/App.jsx @@ -35,6 +35,10 @@ import {AdminProblemsPageImportFromPolygon} from "./pages/admin/problems/AdminPr import ChangeLanguagePage from "./pages/shared/ChangeLanguagePage.jsx"; import RegisterPage from "./pages/shared/RegisterPage.jsx"; import {AdminProblemsPageImportFromJSON} from "./pages/admin/problems/AdminProblemsPageImportFromJSON.jsx"; +import {PostPage} from "./pages/shared/PostPage.jsx"; +import {AdminPostPage} from "./pages/admin/pages/AdminPostPage.jsx"; +import {AdminPostsPageCreate} from "./pages/admin/pages/AdminPostsPageCreate.jsx"; +import {AdminPostsPageEdit} from "./pages/admin/pages/AdminPostsPageEdit.jsx"; import("../node_modules/bootstrap/dist/js/bootstrap.min.js") @@ -51,6 +55,8 @@ function App() { }/> }/> + }/> + }/> }/> }/> @@ -69,10 +75,17 @@ function App() { }/> }/> }/> + + }/> + }/> + }/> + }/> + }/> }/> }/> + }/> }/> }/> diff --git a/FRONTEND_V2/src/components/AdminHeader.jsx b/FRONTEND_V2/src/components/AdminHeader.jsx index 4b16d23..fe8757e 100644 --- a/FRONTEND_V2/src/components/AdminHeader.jsx +++ b/FRONTEND_V2/src/components/AdminHeader.jsx @@ -10,6 +10,7 @@ export const AdminHeader = () => { {t('header.problems')} {t('adminUsers.users')} {t('adminCheckers.checkers')} + {t('header.posts')} {t('header.student_interface')} ); diff --git a/FRONTEND_V2/src/components/form_impl/PostForm.jsx b/FRONTEND_V2/src/components/form_impl/PostForm.jsx new file mode 100644 index 0000000..1e276e4 --- /dev/null +++ b/FRONTEND_V2/src/components/form_impl/PostForm.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import {InputFormElement} from "../forms/InputFormElement.jsx"; +import {useTranslation} from 'react-i18next'; +import {TextareaFormElement} from "../forms/TextareaFormElement.jsx"; +import {CheckboxFormElement} from "../forms/CheckboxFormElement.jsx"; + +export const PostForm = () => { + const {t} = useTranslation(); + return ( + <> + + + + + + + + + ); +}; + diff --git a/FRONTEND_V2/src/locales/en.json b/FRONTEND_V2/src/locales/en.json index fd47999..b4e2818 100644 --- a/FRONTEND_V2/src/locales/en.json +++ b/FRONTEND_V2/src/locales/en.json @@ -30,7 +30,8 @@ "help": "Help", "logout": "Logout", "lang": "Language", - "student_interface": "Student interface" + "student_interface": "Student interface", + "posts": "Posts" }, "statusCodes": { "title": "Program execution statuses", @@ -316,6 +317,21 @@ }, "deleteButton": { "text": "Delete" + }, + "posts": { + "posts": "Posts", + "read": "Read", + "param_title": "Title", + "param_title_help": "Title displayed at main page", + "param_title_required": "Title required", + "param_content": "Content of post", + "param_content_help": "Supports markdown", + "param_content_required": "Content required", + "param_mainpage": "On main page", + "param_mainpage_help": "If selected post visible at main page", + "create": "Create post", + "submit": "Submit", + "edit": "Edit post" } } } diff --git a/FRONTEND_V2/src/locales/ru.json b/FRONTEND_V2/src/locales/ru.json index c1836ea..b34782d 100644 --- a/FRONTEND_V2/src/locales/ru.json +++ b/FRONTEND_V2/src/locales/ru.json @@ -30,7 +30,8 @@ "help": "Помощь", "logout": "Выход", "lang": "Язык", - "student_interface": "интерфейс ученика" + "student_interface": "интерфейс ученика", + "posts": "Посты" }, "statusCodes": { "title": "Статусы выполнения программ", @@ -316,6 +317,22 @@ }, "deleteButton": { "text": "Удалить" + }, + "posts": { + "posts": "Посты", + "read": "Читать", + "param_title": "Заголовок", + "param_title_help": "Заголовок, отображаемый на главной странице", + "param_title_required": "Заголовок обязателен", + "param_content": "Содержимое поста", + "param_content_help": "Поддерживает markdown", + "param_content_required": "Содержимое обязательно", + "param_mainpage": "На главной странице", + "param_mainpage_help": "Если выбрано, пост будет виден на главной странице", + "create": "Создать пост", + "submit": "Отправить", + "edit": "Редактировать пост" } + } } \ No newline at end of file diff --git a/FRONTEND_V2/src/pages/admin/pages/AdminPostPage.jsx b/FRONTEND_V2/src/pages/admin/pages/AdminPostPage.jsx new file mode 100644 index 0000000..3989cfd --- /dev/null +++ b/FRONTEND_V2/src/pages/admin/pages/AdminPostPage.jsx @@ -0,0 +1,72 @@ +import Card from "../../../components/bootstrap/Card.jsx"; +import useCachedGetAPI from "../../../hooks/useGetAPI.js"; +import {useEffect} from "react"; +import {Link} from "react-router-dom"; +import BreadcrumbsElement from "../../../components/BreadcrumbsElement.jsx"; +import BreadcrumbsRoot from "../../../components/BreadcrumpsRoot.jsx"; +import UserLoginRequired from "../../../components/UserLoginRequired.jsx"; +import {AdminHeader} from "../../../components/AdminHeader.jsx"; +import {useTranslation} from 'react-i18next'; + +export const AdminPostPage = () => { + + const [data, update] = useCachedGetAPI("/api/posts", () => { + }, []); + + const { t } = useTranslation(); + + useEffect(() => { + update() + }, []); + + console.debug(data) + + return ( + <> + + + + + + + + + + + + + + + + + + + { + data?.map(elem => { + return ( + + + + + + ) + }) + } + + +
{t('posts.param_title')}{t('posts.param_content')}{t('posts.param_mainpage')}
{elem.id} + + {elem.title} + + + +
+ +
+ {t('posts.create')} +
+
+ + + ); +}; diff --git a/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageCreate.jsx b/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageCreate.jsx new file mode 100644 index 0000000..22156ca --- /dev/null +++ b/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageCreate.jsx @@ -0,0 +1,57 @@ +import useCachedGetAPI from "../../../hooks/useGetAPI.js"; +import React, {useEffect} from "react"; +import BreadcrumbsElement from "../../../components/BreadcrumbsElement.jsx"; +import BreadcrumbsRoot from "../../../components/BreadcrumpsRoot.jsx"; +import UserLoginRequired from "../../../components/UserLoginRequired.jsx"; +import {AdminHeader} from "../../../components/AdminHeader.jsx"; +import {useForm} from "react-hook-form"; +import Card from "../../../components/bootstrap/Card.jsx"; +import {useNavigate} from "react-router-dom"; +import {MasterForm} from "../../../components/forms/MasterForm.jsx"; +import {useTranslation} from 'react-i18next'; +import {axiosInstance} from "../../../utils/settings.js"; +import {PostForm} from "../../../components/form_impl/PostForm.jsx"; + +export const AdminPostsPageCreate = () => { + + const navigate = useNavigate() + const { t } = useTranslation(); + + const [data, update] = useCachedGetAPI("/api/posts", () => { + }, []); + + useEffect(() => { + update() + }, []); + + console.debug(data) + + const form = useForm({}) + + const onSubmit = (data) => { + axiosInstance.post('/api/posts', data) + .then(() => navigate("/admin/champs")) + }; + + return ( + <> + + + + + + + + + +
+

{t('posts.create')}

+ + + + +
+
+ + ); +}; diff --git a/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageEdit.jsx b/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageEdit.jsx new file mode 100644 index 0000000..9b04cd0 --- /dev/null +++ b/FRONTEND_V2/src/pages/admin/pages/AdminPostsPageEdit.jsx @@ -0,0 +1,66 @@ +import useCachedGetAPI from "../../../hooks/useGetAPI.js"; +import React, {useEffect} from "react"; +import BreadcrumbsElement from "../../../components/BreadcrumbsElement.jsx"; +import BreadcrumbsRoot from "../../../components/BreadcrumpsRoot.jsx"; +import UserLoginRequired from "../../../components/UserLoginRequired.jsx"; +import {AdminHeader} from "../../../components/AdminHeader.jsx"; +import {useForm} from "react-hook-form"; +import Card from "../../../components/bootstrap/Card.jsx"; +import {useNavigate, useParams} from "react-router-dom"; +import {MasterForm} from "../../../components/forms/MasterForm.jsx"; +import {useTranslation} from 'react-i18next'; +import {PostForm} from "../../../components/form_impl/PostForm.jsx"; +import {axiosInstance} from "../../../utils/settings.js"; + +export const AdminPostsPageEdit = () => { + + const navigate = useNavigate() + + const {postId} = useParams() + + const [data, update] = useCachedGetAPI(`/api/posts/${postId}`, () => { + }, []); + + useEffect(() => { + form.reset({...data}) + }, [data]); + + useEffect(() => { + update() + }, []); + + + console.debug(data) + + const form = useForm({}) + + const { t } = useTranslation(); + + const onSubmit = (data) => { + axiosInstance.put(`/api/posts/${postId}`, data) + .then(() => navigate("/admin/posts")) + }; + + + return ( + <> + + + + + + + + + +
+

{t('posts.edit')}

+ + + + +
+
+ + ); +}; diff --git a/FRONTEND_V2/src/pages/shared/PostPage.jsx b/FRONTEND_V2/src/pages/shared/PostPage.jsx new file mode 100644 index 0000000..2706c4d --- /dev/null +++ b/FRONTEND_V2/src/pages/shared/PostPage.jsx @@ -0,0 +1,33 @@ +import Card from "../../components/bootstrap/Card.jsx"; +import {useEffect} from "react"; +import UserLoginRequired from "../../components/UserLoginRequired.jsx"; +import useCachedGetAPI from "../../hooks/useGetAPI.js"; +import {useParams} from "react-router-dom"; +import {useTranslation} from "react-i18next"; +import Markdown from "../../components/wraps/Markdown.jsx"; + +export const PostPage = () => { + const {t} = useTranslation(); + const {postId} = useParams(); + const [post, updateData] = useCachedGetAPI(`/api/posts/${postId}`); + + + useEffect(() => { + updateData(); + }, []); + + + return ( + <> + + + +

{post.title}

+
+ + + + + + ); +}; \ No newline at end of file diff --git a/FRONTEND_V2/src/pages/user/ChampsPage.jsx b/FRONTEND_V2/src/pages/user/ChampsPage.jsx index 66b59f9..5d5e240 100644 --- a/FRONTEND_V2/src/pages/user/ChampsPage.jsx +++ b/FRONTEND_V2/src/pages/user/ChampsPage.jsx @@ -17,11 +17,16 @@ const ChampsPage = () => { const [data, update, champsLoading] = useCachedGetAPI("/api/competitions/me", () => { }, []); + + const [posts, updatePosts, loadingPosts] = useCachedGetAPI("/api/posts/main", () => { + }, []); + const [publicChamps, updatePublicChamps, publicChampsLoading] = useCachedGetAPI("/api/competitions/public", () => { }, []); useEffect(() => { update() + updatePosts() updatePublicChamps() }, []); @@ -31,8 +36,29 @@ const ChampsPage = () => { return ( <> + +

+ {t("posts.posts")} +

+
+ + + + { + posts.map((post)=>{ + return
+ +
{post.title}
+ {t("posts.read")} +
+
+ }) + } +
+
+