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
Original file line number Diff line number Diff line change
@@ -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,
)
19 changes: 19 additions & 0 deletions BACKEND_V2/src/main/kotlin/ru/codebattles/backend/dto/PostDto.kt
Original file line number Diff line number Diff line change
@@ -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,
)

Original file line number Diff line number Diff line change
@@ -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<Posts, CreatePostDto>
Original file line number Diff line number Diff line change
@@ -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<Posts, PostDto>
18 changes: 18 additions & 0 deletions BACKEND_V2/src/main/kotlin/ru/codebattles/backend/entity/Posts.kt
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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<Posts, Long> {
fun findByShowAtMainTrue(): List<Posts>
}
Original file line number Diff line number Diff line change
@@ -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<PostDto> {
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<PostDto> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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<PostDto> {
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<PostDto> {
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<Void> {
postService.delete(id)
return ResponseEntity.noContent().build()
}
}
10 changes: 10 additions & 0 deletions BACKEND_V2/src/main/resources/db/migrations/V7__add_posts.sql
Original file line number Diff line number Diff line change
@@ -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)
);
13 changes: 13 additions & 0 deletions FRONTEND_V2/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -51,6 +55,8 @@ function App() {
<Route path="/" element={<LoginPage/>}/>
<Route path="/register" element={<RegisterPage/>}/>

<Route path="/posts/:postId" element={<PostPage/>}/>

<Route path="/problems" element={<ProblemsListPage/>}/>
<Route path="/champs/:compId/problems" element={<ProblemsListPage/>}/>
<Route path="/champs/:compId/problems/:id" element={<SeeProblemPage/>}/>
Expand All @@ -69,10 +75,17 @@ function App() {
<Route path="/admin/problems/import/polygon" element={<AdminProblemsPageImportFromPolygon/>}/>
<Route path="/admin/problems/import/json" element={<AdminProblemsPageImportFromJSON/>}/>
<Route path="/admin/problems/:probId/edit" element={<AdminProblemsPageEdit/>}/>

<Route path="/admin/posts" element={<AdminPostPage/>}/>
<Route path="/admin/posts/create" element={<AdminPostsPageCreate/>}/>
<Route path="/admin/posts/:postId/edit" element={<AdminPostsPageEdit/>}/>

<Route path="/admin/champs" element={<AdminChampsPage/>}/>

<Route path="/admin/checkers" element={<AdminCheckersPage/>}/>
<Route path="/admin/checkers/:checkId/edit" element={<AdminCheckersEditPage/>}/>
<Route path="/admin/checkers/create" element={<AdminCheckersCreatePage/>}/>

<Route path="/admin/champs/:compId/edit" element={<AdminChampsDetailPage/>}/>
<Route path="/admin/champs/create" element={<AdminChampsCreate/>}/>
<Route path="/admin/champs/:compId/edit/users" element={<AdminUsersDetailPage/>}/>
Expand Down
1 change: 1 addition & 0 deletions FRONTEND_V2/src/components/AdminHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const AdminHeader = () => {
<Link to='/admin/problems' className="btn border-0">{t('header.problems')}</Link>
<Link to='/admin/users' className="btn border-0">{t('adminUsers.users')}</Link>
<Link to="/admin/checkers" className="btn">{t('adminCheckers.checkers')}</Link>
<Link to="/admin/posts" className="btn">{t('header.posts')}</Link>
<Link to="/champs" className="btn">{t('header.student_interface')}</Link>
</Card>
);
Expand Down
35 changes: 35 additions & 0 deletions FRONTEND_V2/src/components/form_impl/PostForm.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<InputFormElement
name="title"
displayName={t('posts.param_title')}
helpText={t('posts.param_title_help')}
args={{required: t('posts.param_title_required')}}
/>

<TextareaFormElement
name="content"
displayName={t('posts.param_content')}
helpText={t('posts.param_content_help')}
args={{required: t('posts.param_content_required')}}
/>

<CheckboxFormElement
name="showAtMain"
displayName={t('posts.param_mainpage')}
helpText={t('posts.param_mainpage_help')}
/>


</>
);
};

18 changes: 17 additions & 1 deletion FRONTEND_V2/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
}
19 changes: 18 additions & 1 deletion FRONTEND_V2/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"help": "Помощь",
"logout": "Выход",
"lang": "Язык",
"student_interface": "интерфейс ученика"
"student_interface": "интерфейс ученика",
"posts": "Посты"
},
"statusCodes": {
"title": "Статусы выполнения программ",
Expand Down Expand Up @@ -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": "Редактировать пост"
}

}
}
Loading