diff --git a/GPTutor-Backend/mvnw b/GPTutor-Backend/mvnw old mode 100644 new mode 100755 diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/controllers/TopicSuggestionController.java b/GPTutor-Backend/src/main/java/com/chatgpt/controllers/TopicSuggestionController.java new file mode 100644 index 00000000..fbe76c99 --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/controllers/TopicSuggestionController.java @@ -0,0 +1,54 @@ +package com.chatgpt.controllers; + +import com.chatgpt.entity.database.TopicSuggestion; +import com.chatgpt.entity.requests.CreateTopicSuggestion; +import com.chatgpt.entity.requests.UpdateTopicSuggestion; +import com.chatgpt.services.TopicSuggestionService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +public class TopicSuggestionController { + + @Autowired + TopicSuggestionService topicSuggestionService; + + @PostMapping(path = "/topic-suggestion") + public TopicSuggestion createTopicSuggestion(HttpServletRequest request, @RequestBody CreateTopicSuggestion createTopicSuggestion) { + return topicSuggestionService.createTopicSuggestion((String) request.getAttribute("vkUserId"), createTopicSuggestion); + } + + @GetMapping(path = "/topic-suggestion") + public List getTopicSuggestions(HttpServletRequest request) { + return topicSuggestionService.getTopicSuggestions((String) request.getAttribute("vkUserId")); + } + + @GetMapping(path = "/topic-suggestion/all") + public List getAllTopicSuggestions() { + return topicSuggestionService.getAllTopicSuggestions(); + } + + @GetMapping(path = "/topic-suggestion/status/{status}") + public List getTopicSuggestionsByStatus(@PathVariable("status") TopicSuggestion.SuggestionStatus status) { + return topicSuggestionService.getTopicSuggestionsByStatus(status); + } + + @GetMapping(path = "/topic-suggestion/type/{type}") + public List getTopicSuggestionsByType(@PathVariable("type") TopicSuggestion.TopicType type) { + return topicSuggestionService.getTopicSuggestionsByType(type); + } + + @DeleteMapping(path = "/topic-suggestion/{id}") + public void deleteTopicSuggestion(HttpServletRequest request, @PathVariable("id") UUID topicSuggestionId) { + topicSuggestionService.deleteTopicSuggestion((String) request.getAttribute("vkUserId"), topicSuggestionId); + } + + @PutMapping(path = "/topic-suggestion") + public void updateTopicSuggestion(HttpServletRequest request, @RequestBody UpdateTopicSuggestion updateTopicSuggestion) { + topicSuggestionService.updateTopicSuggestion((String) request.getAttribute("vkUserId"), updateTopicSuggestion); + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/TopicSuggestion.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/TopicSuggestion.java new file mode 100644 index 00000000..8c9a53ae --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/TopicSuggestion.java @@ -0,0 +1,114 @@ +package com.chatgpt.entity.database; + +import com.chatgpt.entity.VkUser; +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +public class TopicSuggestion { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne() + private VkUser vkUser; + + private String title; + + private String description; + + @Enumerated(EnumType.STRING) + private TopicType type; + + @Enumerated(EnumType.STRING) + private SuggestionStatus status; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + public enum TopicType { + LESSON, + CONVERSATION_STARTER, + ADDITIONAL_REQUEST + } + + public enum SuggestionStatus { + PENDING, + APPROVED, + REJECTED + } + + public TopicSuggestion() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + this.status = SuggestionStatus.PENDING; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public VkUser getVkUser() { + return vkUser; + } + + public void setVkUser(VkUser vkUser) { + this.vkUser = vkUser; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TopicType getType() { + return type; + } + + public void setType(TopicType type) { + this.type = type; + } + + public SuggestionStatus getStatus() { + return status; + } + + public void setStatus(SuggestionStatus status) { + this.status = status; + this.updatedAt = LocalDateTime.now(); + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateTopicSuggestion.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateTopicSuggestion.java new file mode 100644 index 00000000..12db64f4 --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateTopicSuggestion.java @@ -0,0 +1,33 @@ +package com.chatgpt.entity.requests; + +import com.chatgpt.entity.database.TopicSuggestion; + +public class CreateTopicSuggestion { + private String title; + private String description; + private TopicSuggestion.TopicType type; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TopicSuggestion.TopicType getType() { + return type; + } + + public void setType(TopicSuggestion.TopicType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateTopicSuggestion.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateTopicSuggestion.java new file mode 100644 index 00000000..d9cd6bcf --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateTopicSuggestion.java @@ -0,0 +1,53 @@ +package com.chatgpt.entity.requests; + +import com.chatgpt.entity.database.TopicSuggestion; + +import java.util.UUID; + +public class UpdateTopicSuggestion { + private UUID id; + private String title; + private String description; + private TopicSuggestion.TopicType type; + private TopicSuggestion.SuggestionStatus status; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TopicSuggestion.TopicType getType() { + return type; + } + + public void setType(TopicSuggestion.TopicType type) { + this.type = type; + } + + public TopicSuggestion.SuggestionStatus getStatus() { + return status; + } + + public void setStatus(TopicSuggestion.SuggestionStatus status) { + this.status = status; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/repositories/TopicSuggestionRepository.java b/GPTutor-Backend/src/main/java/com/chatgpt/repositories/TopicSuggestionRepository.java new file mode 100644 index 00000000..5f797a0f --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/repositories/TopicSuggestionRepository.java @@ -0,0 +1,18 @@ +package com.chatgpt.repositories; + +import com.chatgpt.entity.database.TopicSuggestion; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.UUID; + +public interface TopicSuggestionRepository extends CrudRepository { + List findAllByVkUserId(UUID vkUserId); + Page findAllByVkUserId(UUID vkUserId, PageRequest pageable); + List findAllByStatus(TopicSuggestion.SuggestionStatus status); + List findAllByType(TopicSuggestion.TopicType type); + List findAllByVkUserIdAndStatus(UUID vkUserId, TopicSuggestion.SuggestionStatus status); + void deleteAllByVkUserId(UUID vkUserId); +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/services/TopicSuggestionService.java b/GPTutor-Backend/src/main/java/com/chatgpt/services/TopicSuggestionService.java new file mode 100644 index 00000000..e1ec833e --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/services/TopicSuggestionService.java @@ -0,0 +1,102 @@ +package com.chatgpt.services; + +import com.chatgpt.entity.VkUser; +import com.chatgpt.entity.database.TopicSuggestion; +import com.chatgpt.entity.requests.CreateTopicSuggestion; +import com.chatgpt.entity.requests.UpdateTopicSuggestion; +import com.chatgpt.repositories.TopicSuggestionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.UUID; + +@Service +public class TopicSuggestionService { + @Autowired + UserService userService; + + @Autowired + TopicSuggestionRepository topicSuggestionRepository; + + public TopicSuggestion createTopicSuggestion(String vkUserId, CreateTopicSuggestion createTopicSuggestion) { + var user = userService.getOrCreateVkUser(vkUserId); + return saveTopicSuggestion( + user, + createTopicSuggestion.getTitle(), + createTopicSuggestion.getDescription(), + createTopicSuggestion.getType() + ); + } + + public List getTopicSuggestions(String vkUserId) { + var user = userService.getOrCreateVkUser(vkUserId); + return topicSuggestionRepository.findAllByVkUserId(user.getId()); + } + + public List getAllTopicSuggestions() { + return (List) topicSuggestionRepository.findAll(); + } + + public List getTopicSuggestionsByStatus(TopicSuggestion.SuggestionStatus status) { + return topicSuggestionRepository.findAllByStatus(status); + } + + public List getTopicSuggestionsByType(TopicSuggestion.TopicType type) { + return topicSuggestionRepository.findAllByType(type); + } + + public void deleteTopicSuggestion(String vkUserId, UUID topicSuggestionId) { + var user = userService.getOrCreateVkUser(vkUserId); + var foundTopicSuggestion = topicSuggestionRepository.findById(topicSuggestionId); + + foundTopicSuggestion.ifPresent(topicSuggestion -> checkAccess(user, topicSuggestion)); + + topicSuggestionRepository.deleteById(topicSuggestionId); + } + + public void updateTopicSuggestion(String vkUserId, UpdateTopicSuggestion updateTopicSuggestion) { + var user = userService.getOrCreateVkUser(vkUserId); + var foundTopicSuggestion = topicSuggestionRepository.findById(updateTopicSuggestion.getId()); + + foundTopicSuggestion.ifPresent(topicSuggestion -> { + checkAccess(user, topicSuggestion); + + if (updateTopicSuggestion.getTitle() != null) { + topicSuggestion.setTitle(updateTopicSuggestion.getTitle()); + } + if (updateTopicSuggestion.getDescription() != null) { + topicSuggestion.setDescription(updateTopicSuggestion.getDescription()); + } + if (updateTopicSuggestion.getType() != null) { + topicSuggestion.setType(updateTopicSuggestion.getType()); + } + if (updateTopicSuggestion.getStatus() != null) { + topicSuggestion.setStatus(updateTopicSuggestion.getStatus()); + } + + topicSuggestionRepository.save(topicSuggestion); + }); + } + + private void checkAccess(VkUser user, TopicSuggestion topicSuggestion) { + if (!user.getId().equals(topicSuggestion.getVkUser().getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + } + + private TopicSuggestion saveTopicSuggestion(VkUser user, String title, String description, TopicSuggestion.TopicType type) { + var topicSuggestion = new TopicSuggestion(); + + topicSuggestion.setVkUser(user); + topicSuggestion.setTitle(title); + topicSuggestion.setDescription(description); + topicSuggestion.setType(type); + + topicSuggestionRepository.save(topicSuggestion); + + return topicSuggestion; + } +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/App.tsx b/GPTutor-Frontend/src/App.tsx index f7bc2896..e59b7d5d 100644 --- a/GPTutor-Frontend/src/App.tsx +++ b/GPTutor-Frontend/src/App.tsx @@ -74,6 +74,8 @@ import ApplicationInfoHumor from "./modals/ApplicationInfoHumor/ApplicationInfoH import { BingPanel } from "$/panels/BingPanel"; import VKDocQuestionPanel from "./panels/VKDocQuestionPanel/VKDocQestionPanel"; import { VkDocQuestionRequest } from "$/panels/VkDocQuestionRequest"; +import TopicSuggestions from "./panels/TopicSuggestions/TopicSuggestions"; +import TopicSuggestionModal from "./modals/TopicSuggestionModal/TopicSuggestionModal"; import { retrieveLaunchParams } from "@telegram-apps/sdk"; const App = () => { @@ -156,6 +158,7 @@ const App = () => { + } > @@ -198,6 +201,7 @@ const App = () => { + )} diff --git a/GPTutor-Frontend/src/NavigationContext.tsx b/GPTutor-Frontend/src/NavigationContext.tsx index 5c363ab7..848a58e5 100644 --- a/GPTutor-Frontend/src/NavigationContext.tsx +++ b/GPTutor-Frontend/src/NavigationContext.tsx @@ -57,6 +57,8 @@ export type NavigationContextType = { goToAnecdoteMain: () => void; goToBingPanel: () => void; goToVkDocQuestionRequest: () => void; + goToTopicSuggestions: () => void; + goToTopicSuggestionModal: () => void; openAlert: (data: AlertType) => void; alert: AlertType; isForbidden: boolean; @@ -223,6 +225,14 @@ export function NavigationContextProvider({ router.pushPage(RoutingPages.vkDocQuestionRequest); }; + const goToTopicSuggestions = () => { + router.pushPage(RoutingPages.topicSuggestions); + }; + + const goToTopicSuggestionModal = () => { + router.pushModal(Modals.topicSuggestionModal); + }; + return ( { @@ -19,7 +17,7 @@ export function getAdditionalRequest(): Promise { } export function deleteAdditionalRequestById(id: string) { - return httpService.delete("additional-request"); + return httpService.delete(`additional-request/${id}`); } export function updateAdditionalRequestById(params: AdditionalRequest) { diff --git a/GPTutor-Frontend/src/api/topicSuggestion.ts b/GPTutor-Frontend/src/api/topicSuggestion.ts new file mode 100644 index 00000000..eba446c6 --- /dev/null +++ b/GPTutor-Frontend/src/api/topicSuggestion.ts @@ -0,0 +1,50 @@ +import { + TopicSuggestion, + TopicSuggestionCreate, + TopicSuggestionUpdate, + TopicType, + SuggestionStatus, +} from "$/entity/topicSuggestion/types"; +import { httpService } from "$/services/HttpService"; + +export function createTopicSuggestion( + params: TopicSuggestionCreate +): Promise { + return httpService + .post("topic-suggestion", params) + .then((res) => res.json()); +} + +export function getTopicSuggestions(): Promise { + return httpService.get("topic-suggestion").then((res) => res.json()); +} + +export function getAllTopicSuggestions(): Promise { + return httpService.get("topic-suggestion/all").then((res) => res.json()); +} + +export function getTopicSuggestionsByStatus( + status: SuggestionStatus +): Promise { + return httpService + .get(`topic-suggestion/status/${status}`) + .then((res) => res.json()); +} + +export function getTopicSuggestionsByType( + type: TopicType +): Promise { + return httpService + .get(`topic-suggestion/type/${type}`) + .then((res) => res.json()); +} + +export function deleteTopicSuggestionById(id: string) { + return httpService.delete(`topic-suggestion/${id}`); +} + +export function updateTopicSuggestionById(params: TopicSuggestionUpdate) { + return httpService + .put("topic-suggestion", params) + .then((res) => res.json()); +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/routing/routing.ts b/GPTutor-Frontend/src/entity/routing/routing.ts index 063963dd..60b81095 100644 --- a/GPTutor-Frontend/src/entity/routing/routing.ts +++ b/GPTutor-Frontend/src/entity/routing/routing.ts @@ -41,6 +41,8 @@ export enum RoutingPages { vkDocQuestionPanel = "/vk-doc-question-panel", vkDocQuestionRequest = "/vk-doc-question-request", + + topicSuggestions = "/topic-suggestions", } export enum Views { @@ -89,6 +91,8 @@ export enum Panels { vkDocQuestionPanel = "vk-doc-question-panel", vkDocQuestionRequest = "vk-doc-question-request", + + topicSuggestions = "topic-suggestions", } export enum Modals { @@ -103,4 +107,6 @@ export enum Modals { alert = "alert", imageDonut = "image-donut", + + topicSuggestionModal = "topic-suggestion-modal", } diff --git a/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestionItem.ts b/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestionItem.ts new file mode 100644 index 00000000..0a6485e3 --- /dev/null +++ b/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestionItem.ts @@ -0,0 +1,30 @@ +import { sig } from "dignals"; +import { TopicType, SuggestionStatus } from "./types"; + +export class TopicSuggestionItem { + id: string; + title$ = sig(""); + description$ = sig(""); + type$ = sig(TopicType.CONVERSATION_STARTER); + status$ = sig(SuggestionStatus.PENDING); + createdAt: string; + updatedAt: string; + + constructor( + id: string, + title: string, + description: string, + type: TopicType, + status: SuggestionStatus, + createdAt: string, + updatedAt: string + ) { + this.id = id; + this.title$.set(title); + this.description$.set(description); + this.type$.set(type); + this.status$.set(status); + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestions.ts b/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestions.ts new file mode 100644 index 00000000..b14c57da --- /dev/null +++ b/GPTutor-Frontend/src/entity/topicSuggestion/TopicSuggestions.ts @@ -0,0 +1,70 @@ +import { sig } from "dignals"; +import { TopicSuggestionItem } from "./TopicSuggestionItem"; +import { + createTopicSuggestion, + deleteTopicSuggestionById, + getTopicSuggestions, + updateTopicSuggestionById, +} from "$/api/topicSuggestion"; +import { TopicSuggestionCreate, TopicSuggestionUpdate } from "./types"; + +class TopicSuggestions { + suggestions$ = sig(new Set()); + + async init() { + await this.initSuggestions(); + } + + async initSuggestions() { + const suggestions = await getTopicSuggestions(); + + this.suggestions$.set( + new Set( + suggestions.map( + ({ id, title, description, type, status, createdAt, updatedAt }) => + new TopicSuggestionItem(id, title, description, type, status, createdAt, updatedAt) + ) + ) + ); + } + + async createSuggestion(suggestion: TopicSuggestionCreate) { + const topicSuggestion = await createTopicSuggestion(suggestion); + + const set = new Set(this.suggestions$.get()); + set.add( + new TopicSuggestionItem( + topicSuggestion.id, + suggestion.title, + suggestion.description, + suggestion.type, + topicSuggestion.status, + topicSuggestion.createdAt, + topicSuggestion.updatedAt + ) + ); + + this.suggestions$.set(set); + } + + async deleteSuggestion(suggestion: TopicSuggestionItem) { + await deleteTopicSuggestionById(suggestion.id); + + const set = new Set(this.suggestions$.get()); + set.delete(suggestion); + + this.suggestions$.set(set); + } + + updateTopicSuggestion = async (suggestion: TopicSuggestionItem) => { + await updateTopicSuggestionById({ + id: suggestion.id, + title: suggestion.title$.get(), + description: suggestion.description$.get(), + type: suggestion.type$.get(), + status: suggestion.status$.get(), + }); + }; +} + +export const topicSuggestions = new TopicSuggestions(); \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/topicSuggestion/index.ts b/GPTutor-Frontend/src/entity/topicSuggestion/index.ts new file mode 100644 index 00000000..0ec51fd4 --- /dev/null +++ b/GPTutor-Frontend/src/entity/topicSuggestion/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./TopicSuggestionItem"; +export * from "./TopicSuggestions"; \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/topicSuggestion/types.ts b/GPTutor-Frontend/src/entity/topicSuggestion/types.ts new file mode 100644 index 00000000..053f2714 --- /dev/null +++ b/GPTutor-Frontend/src/entity/topicSuggestion/types.ts @@ -0,0 +1,35 @@ +export enum TopicType { + LESSON = "LESSON", + CONVERSATION_STARTER = "CONVERSATION_STARTER", + ADDITIONAL_REQUEST = "ADDITIONAL_REQUEST", +} + +export enum SuggestionStatus { + PENDING = "PENDING", + APPROVED = "APPROVED", + REJECTED = "REJECTED", +} + +export interface TopicSuggestion { + id: string; + title: string; + description: string; + type: TopicType; + status: SuggestionStatus; + createdAt: string; + updatedAt: string; +} + +export interface TopicSuggestionCreate { + title: string; + description: string; + type: TopicType; +} + +export interface TopicSuggestionUpdate { + id: string; + title?: string; + description?: string; + type?: TopicType; + status?: SuggestionStatus; +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/modals/TopicSuggestionModal/TopicSuggestionModal.tsx b/GPTutor-Frontend/src/modals/TopicSuggestionModal/TopicSuggestionModal.tsx new file mode 100644 index 00000000..dc82d1f5 --- /dev/null +++ b/GPTutor-Frontend/src/modals/TopicSuggestionModal/TopicSuggestionModal.tsx @@ -0,0 +1,118 @@ +import React, { useState } from "react"; +import { + Button, + Div, + FormItem, + FormLayout, + Input, + ModalPage, + ModalPageHeader, + Select, + Textarea, +} from "@vkontakte/vkui"; +import { useNavigationContext } from "$/NavigationContext"; +import { topicSuggestions, TopicType } from "$/entity/topicSuggestion"; + +interface IProps { + id: string; +} + +const topicTypeOptions = [ + { label: "Тема урока", value: TopicType.LESSON }, + { label: "Стартер разговора", value: TopicType.CONVERSATION_STARTER }, + { label: "Дополнительный запрос", value: TopicType.ADDITIONAL_REQUEST }, +]; + +function TopicSuggestionModal({ id }: IProps) { + const { goBack } = useNavigationContext(); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [type, setType] = useState(TopicType.CONVERSATION_STARTER); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + if (!title.trim() || !description.trim()) { + return; + } + + setIsLoading(true); + try { + await topicSuggestions.createSuggestion({ + title: title.trim(), + description: description.trim(), + type, + }); + + setTitle(""); + setDescription(""); + setType(TopicType.CONVERSATION_STARTER); + goBack(); + } catch (error) { + console.error("Error creating topic suggestion:", error); + } finally { + setIsLoading(false); + } + }; + + const isValid = title.trim().length > 0 && description.trim().length > 0; + + return ( + + Предложить тему + + } + > + + + setTitle(e.target.value)} + placeholder="Введите название темы" + /> + + + +