From 45ffc4fe1efc426f798c1e24e45a3eb286d609ac Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 21:39:34 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #24 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/deep-assistant/GPTutor/issues/24 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..11ebb2dd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/deep-assistant/GPTutor/issues/24 +Your prepared branch: issue-24-41f551e8 +Your prepared working directory: /tmp/gh-issue-solver-1757529555195 + +Proceed. \ No newline at end of file From 8bbebaf680cc798200a632da656abd60b0401937 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 21:39:50 +0300 Subject: [PATCH 2/3] Remove CLAUDE.md - PR created successfully --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 11ebb2dd..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/deep-assistant/GPTutor/issues/24 -Your prepared branch: issue-24-41f551e8 -Your prepared working directory: /tmp/gh-issue-solver-1757529555195 - -Proceed. \ No newline at end of file From 000c8c36d1db5488cc05be2b9629612fdca1357e Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 21:49:29 +0300 Subject: [PATCH 3/3] =?UTF-8?q?#24=20:=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB?= =?UTF-8?q?=D0=BE=D0=B6=D0=B8=D1=82=D1=8C=20=D1=83=D1=80=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализована полная функциональность для предложения уроков пользователями: Backend изменения: - Создана сущность LessonSuggestion с полями: название, описание, категория, содержание, статус - Добавлен LessonSuggestionController с REST API для CRUD операций - Реализован LessonSuggestionService с бизнес-логикой - Создан LessonSuggestionRepository для работы с БД - Добавлена миграция V25__Create_Lesson_Suggestion.sql Frontend изменения: - Создан API клиент для работы с предложениями уроков - Реализован компонент LessonSuggestion с формой создания и списком предложений - Добавлена кнопка "Предложить урок" в список уроков каждой категории - Настроена навигация и роутинг для новой страницы - Добавлены TypeScript типы для lesson suggestions Пользователи теперь могут: - Предлагать новые уроки с названием, описанием, категорией и содержанием - Просматривать свои предложения со статусами (на рассмотрении/одобрено/отклонено) - Переходить к форме предложения урока из любой категории уроков 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../LessonSuggestionController.java | 49 +++ .../entity/database/LessonSuggestion.java | 106 +++++++ .../CreateLessonSuggestionRequest.java | 40 +++ .../UpdateLessonSuggestionRequest.java | 62 ++++ .../LessonSuggestionRepository.java | 16 + .../services/LessonSuggestionService.java | 96 ++++++ .../V25__Create_Lesson_Suggestion.sql | 12 + GPTutor-Frontend/src/App.tsx | 2 + GPTutor-Frontend/src/NavigationContext.tsx | 6 + GPTutor-Frontend/src/api/lessonSuggestion.ts | 40 +++ .../src/entity/lessonSuggestion/types.ts | 25 ++ .../src/entity/routing/routing.ts | 2 + .../src/panels/Chapters/Lessons/Lessons.tsx | 17 +- .../LessonSuggestion.module.css | 66 ++++ .../LessonSuggestion/LessonSuggestion.tsx | 285 ++++++++++++++++++ .../src/panels/LessonSuggestion/index.ts | 1 + 16 files changed, 823 insertions(+), 2 deletions(-) create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/controllers/LessonSuggestionController.java create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/entity/database/LessonSuggestion.java create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateLessonSuggestionRequest.java create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateLessonSuggestionRequest.java create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/repositories/LessonSuggestionRepository.java create mode 100644 GPTutor-Backend/src/main/java/com/chatgpt/services/LessonSuggestionService.java create mode 100644 GPTutor-Backend/src/main/resources/db/migration/V25__Create_Lesson_Suggestion.sql create mode 100644 GPTutor-Frontend/src/api/lessonSuggestion.ts create mode 100644 GPTutor-Frontend/src/entity/lessonSuggestion/types.ts create mode 100644 GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.module.css create mode 100644 GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.tsx create mode 100644 GPTutor-Frontend/src/panels/LessonSuggestion/index.ts diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/controllers/LessonSuggestionController.java b/GPTutor-Backend/src/main/java/com/chatgpt/controllers/LessonSuggestionController.java new file mode 100644 index 00000000..32b4eaf0 --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/controllers/LessonSuggestionController.java @@ -0,0 +1,49 @@ +package com.chatgpt.controllers; + +import com.chatgpt.entity.database.LessonSuggestion; +import com.chatgpt.entity.requests.CreateLessonSuggestionRequest; +import com.chatgpt.entity.requests.UpdateLessonSuggestionRequest; +import com.chatgpt.services.LessonSuggestionService; +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 LessonSuggestionController { + + @Autowired + LessonSuggestionService lessonSuggestionService; + + @PostMapping(path = "/lesson-suggestion") + public LessonSuggestion createLessonSuggestion(HttpServletRequest request, @RequestBody CreateLessonSuggestionRequest createLessonSuggestionRequest) { + return lessonSuggestionService.createLessonSuggestion((String) request.getAttribute("vkUserId"), createLessonSuggestionRequest); + } + + @GetMapping(path = "/lesson-suggestion") + public List getLessonSuggestions(HttpServletRequest request) { + return lessonSuggestionService.getLessonSuggestions((String) request.getAttribute("vkUserId")); + } + + @GetMapping(path = "/lesson-suggestion/all") + public List getAllLessonSuggestions() { + return lessonSuggestionService.getAllLessonSuggestions(); + } + + @GetMapping(path = "/lesson-suggestion/status/{status}") + public List getLessonSuggestionsByStatus(@PathVariable("status") LessonSuggestion.SuggestionStatus status) { + return lessonSuggestionService.getLessonSuggestionsByStatus(status); + } + + @DeleteMapping(path = "/lesson-suggestion/{id}") + public void deleteLessonSuggestion(HttpServletRequest request, @PathVariable("id") UUID lessonSuggestionId) { + lessonSuggestionService.deleteLessonSuggestion((String) request.getAttribute("vkUserId"), lessonSuggestionId); + } + + @PutMapping(path = "/lesson-suggestion") + public void updateLessonSuggestion(HttpServletRequest request, @RequestBody UpdateLessonSuggestionRequest updateLessonSuggestionRequest) { + lessonSuggestionService.updateLessonSuggestion((String) request.getAttribute("vkUserId"), updateLessonSuggestionRequest); + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/LessonSuggestion.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/LessonSuggestion.java new file mode 100644 index 00000000..f10bf7ab --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/database/LessonSuggestion.java @@ -0,0 +1,106 @@ +package com.chatgpt.entity.database; + +import com.chatgpt.entity.VkUser; +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +public class LessonSuggestion { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne() + private VkUser vkUser; + + private String lessonName; + + private String description; + + private String category; + + @Column(columnDefinition = "TEXT") + private String content; + + private LocalDateTime createdAt; + + @Enumerated(EnumType.STRING) + private SuggestionStatus status; + + public enum SuggestionStatus { + PENDING, + APPROVED, + REJECTED + } + + public LessonSuggestion() { + this.createdAt = 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 getLessonName() { + return lessonName; + } + + public void setLessonName(String lessonName) { + this.lessonName = lessonName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public SuggestionStatus getStatus() { + return status; + } + + public void setStatus(SuggestionStatus status) { + this.status = status; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateLessonSuggestionRequest.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateLessonSuggestionRequest.java new file mode 100644 index 00000000..2a4f9210 --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/CreateLessonSuggestionRequest.java @@ -0,0 +1,40 @@ +package com.chatgpt.entity.requests; + +public class CreateLessonSuggestionRequest { + private String lessonName; + private String description; + private String category; + private String content; + + public String getLessonName() { + return lessonName; + } + + public void setLessonName(String lessonName) { + this.lessonName = lessonName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateLessonSuggestionRequest.java b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateLessonSuggestionRequest.java new file mode 100644 index 00000000..e1b61f2f --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/entity/requests/UpdateLessonSuggestionRequest.java @@ -0,0 +1,62 @@ +package com.chatgpt.entity.requests; + +import com.chatgpt.entity.database.LessonSuggestion; + +import java.util.UUID; + +public class UpdateLessonSuggestionRequest { + private UUID id; + private String lessonName; + private String description; + private String category; + private String content; + private LessonSuggestion.SuggestionStatus status; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getLessonName() { + return lessonName; + } + + public void setLessonName(String lessonName) { + this.lessonName = lessonName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LessonSuggestion.SuggestionStatus getStatus() { + return status; + } + + public void setStatus(LessonSuggestion.SuggestionStatus status) { + this.status = status; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/repositories/LessonSuggestionRepository.java b/GPTutor-Backend/src/main/java/com/chatgpt/repositories/LessonSuggestionRepository.java new file mode 100644 index 00000000..bcdb6e4d --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/repositories/LessonSuggestionRepository.java @@ -0,0 +1,16 @@ +package com.chatgpt.repositories; + +import com.chatgpt.entity.database.LessonSuggestion; +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 LessonSuggestionRepository extends CrudRepository { + List findAllByVkUserId(UUID vkId); + Page findAllByVkUserId(UUID vkId, PageRequest pageable); + List findAllByStatus(LessonSuggestion.SuggestionStatus status); + void deleteAllByVkUserId(UUID vkId); +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/services/LessonSuggestionService.java b/GPTutor-Backend/src/main/java/com/chatgpt/services/LessonSuggestionService.java new file mode 100644 index 00000000..81141802 --- /dev/null +++ b/GPTutor-Backend/src/main/java/com/chatgpt/services/LessonSuggestionService.java @@ -0,0 +1,96 @@ +package com.chatgpt.services; + +import com.chatgpt.entity.VkUser; +import com.chatgpt.entity.database.LessonSuggestion; +import com.chatgpt.entity.requests.CreateLessonSuggestionRequest; +import com.chatgpt.entity.requests.UpdateLessonSuggestionRequest; +import com.chatgpt.repositories.LessonSuggestionRepository; +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 LessonSuggestionService { + @Autowired + UserService userService; + + @Autowired + LessonSuggestionRepository lessonSuggestionRepository; + + public LessonSuggestion createLessonSuggestion(String vkUserId, CreateLessonSuggestionRequest createLessonSuggestionRequest) { + var user = userService.getOrCreateVkUser(vkUserId); + return saveLessonSuggestion( + user, + createLessonSuggestionRequest.getLessonName(), + createLessonSuggestionRequest.getDescription(), + createLessonSuggestionRequest.getCategory(), + createLessonSuggestionRequest.getContent() + ); + } + + public List getLessonSuggestions(String vkUserId) { + var user = userService.getOrCreateVkUser(vkUserId); + return lessonSuggestionRepository.findAllByVkUserId(user.getId()); + } + + public List getAllLessonSuggestions() { + return (List) lessonSuggestionRepository.findAll(); + } + + public List getLessonSuggestionsByStatus(LessonSuggestion.SuggestionStatus status) { + return lessonSuggestionRepository.findAllByStatus(status); + } + + public void deleteLessonSuggestion(String vkUserId, UUID lessonSuggestionId) { + var user = userService.getOrCreateVkUser(vkUserId); + var foundLessonSuggestion = lessonSuggestionRepository.findById(lessonSuggestionId); + + foundLessonSuggestion.ifPresent(lessonSuggestion -> checkAccess(user, lessonSuggestion)); + + lessonSuggestionRepository.deleteById(lessonSuggestionId); + } + + public void updateLessonSuggestion(String vkUserId, UpdateLessonSuggestionRequest updateLessonSuggestionRequest) { + var user = userService.getOrCreateVkUser(vkUserId); + var foundLessonSuggestion = lessonSuggestionRepository.findById(updateLessonSuggestionRequest.getId()); + + foundLessonSuggestion.ifPresent(lessonSuggestion -> { + checkAccess(user, lessonSuggestion); + + lessonSuggestion.setLessonName(updateLessonSuggestionRequest.getLessonName()); + lessonSuggestion.setDescription(updateLessonSuggestionRequest.getDescription()); + lessonSuggestion.setCategory(updateLessonSuggestionRequest.getCategory()); + lessonSuggestion.setContent(updateLessonSuggestionRequest.getContent()); + + if (updateLessonSuggestionRequest.getStatus() != null) { + lessonSuggestion.setStatus(updateLessonSuggestionRequest.getStatus()); + } + + lessonSuggestionRepository.save(lessonSuggestion); + }); + } + + private void checkAccess(VkUser user, LessonSuggestion lessonSuggestion) { + if (!user.getId().equals(lessonSuggestion.getVkUser().getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + } + + private LessonSuggestion saveLessonSuggestion(VkUser user, String lessonName, String description, String category, String content) { + var lessonSuggestion = new LessonSuggestion(); + + lessonSuggestion.setVkUser(user); + lessonSuggestion.setLessonName(lessonName); + lessonSuggestion.setDescription(description); + lessonSuggestion.setCategory(category); + lessonSuggestion.setContent(content); + + lessonSuggestionRepository.save(lessonSuggestion); + + return lessonSuggestion; + } +} \ No newline at end of file diff --git a/GPTutor-Backend/src/main/resources/db/migration/V25__Create_Lesson_Suggestion.sql b/GPTutor-Backend/src/main/resources/db/migration/V25__Create_Lesson_Suggestion.sql new file mode 100644 index 00000000..f487dec7 --- /dev/null +++ b/GPTutor-Backend/src/main/resources/db/migration/V25__Create_Lesson_Suggestion.sql @@ -0,0 +1,12 @@ +CREATE TABLE lesson_suggestion +( + id UUID NOT NULL, + vk_user_id UUID NOT NULL, + lesson_name VARCHAR(255), + description VARCHAR(1000), + category VARCHAR(255), + content TEXT, + created_at TIMESTAMP, + status VARCHAR(20) DEFAULT 'PENDING', + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/GPTutor-Frontend/src/App.tsx b/GPTutor-Frontend/src/App.tsx index f7bc2896..e68b51e4 100644 --- a/GPTutor-Frontend/src/App.tsx +++ b/GPTutor-Frontend/src/App.tsx @@ -67,6 +67,7 @@ import { GPTutorProfile } from "$/panels/GPTutorProfile"; import { transformVKBridgeAdaptivity } from "$/utility/strings"; import { MermaidPage } from "$/panels/MermaidPage"; import { AdditionalRequests } from "$/panels/AdditionalRequests"; +import { LessonSuggestion } from "$/panels/LessonSuggestion"; import { AnecdoteMain } from "$/panels/AnecdoteMain"; import AnecdoteGeneration from "./panels/AnecdoteGeneration/AnecdoteGeneration"; import { AnecdoteNews } from "$/panels/AnecdoteNews"; @@ -192,6 +193,7 @@ const App = () => { + diff --git a/GPTutor-Frontend/src/NavigationContext.tsx b/GPTutor-Frontend/src/NavigationContext.tsx index 5c363ab7..fe0c30fb 100644 --- a/GPTutor-Frontend/src/NavigationContext.tsx +++ b/GPTutor-Frontend/src/NavigationContext.tsx @@ -52,6 +52,7 @@ export type NavigationContextType = { goToMermaidPage: () => void; goToAnecdoteGeneration: () => void; goToAdditionalRequest: () => void; + goToLessonSuggestion: () => void; openApplicationInfoHumor: () => void; goToAnecdoteNews: () => void; goToAnecdoteMain: () => void; @@ -204,6 +205,10 @@ export function NavigationContextProvider({ router.pushPage(RoutingPages.additionalRequest); }; + const goToLessonSuggestion = () => { + router.pushPage(RoutingPages.lessonSuggestion); + }; + const goToAnecdoteGeneration = () => { router.pushPage(RoutingPages.anecdoteGeneration); }; @@ -260,6 +265,7 @@ export function NavigationContextProvider({ goToGPTutorProfile, goToMermaidPage, goToAdditionalRequest, + goToLessonSuggestion, goToAnecdoteGeneration, openApplicationInfoHumor, goToAnecdoteNews, diff --git a/GPTutor-Frontend/src/api/lessonSuggestion.ts b/GPTutor-Frontend/src/api/lessonSuggestion.ts new file mode 100644 index 00000000..4e9a37cb --- /dev/null +++ b/GPTutor-Frontend/src/api/lessonSuggestion.ts @@ -0,0 +1,40 @@ +import { + LessonSuggestion, + LessonSuggestionCreate, + LessonSuggestionUpdate, +} from "$/entity/lessonSuggestion/types"; +import { httpService } from "$/services/HttpService"; + +export function createLessonSuggestion( + params: LessonSuggestionCreate +): Promise { + return httpService + .post("lesson-suggestion", params) + .then((res) => res.json()); +} + +export function getLessonSuggestions(): Promise { + return httpService.get("lesson-suggestion").then((res) => res.json()); +} + +export function getAllLessonSuggestions(): Promise { + return httpService.get("lesson-suggestion/all").then((res) => res.json()); +} + +export function getLessonSuggestionsByStatus( + status: "PENDING" | "APPROVED" | "REJECTED" +): Promise { + return httpService + .get(`lesson-suggestion/status/${status}`) + .then((res) => res.json()); +} + +export function deleteLessonSuggestionById(id: string) { + return httpService.delete(`lesson-suggestion/${id}`); +} + +export function updateLessonSuggestion(params: LessonSuggestionUpdate) { + return httpService + .put("lesson-suggestion", params) + .then((res) => res.json()); +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/lessonSuggestion/types.ts b/GPTutor-Frontend/src/entity/lessonSuggestion/types.ts new file mode 100644 index 00000000..01c2e021 --- /dev/null +++ b/GPTutor-Frontend/src/entity/lessonSuggestion/types.ts @@ -0,0 +1,25 @@ +export interface LessonSuggestion { + id: string; + lessonName: string; + description: string; + category: string; + content: string; + createdAt: string; + status: "PENDING" | "APPROVED" | "REJECTED"; +} + +export interface LessonSuggestionCreate { + lessonName: string; + description: string; + category: string; + content: string; +} + +export interface LessonSuggestionUpdate { + id: string; + lessonName: string; + description: string; + category: string; + content: string; + status?: "PENDING" | "APPROVED" | "REJECTED"; +} \ 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..597cd044 100644 --- a/GPTutor-Frontend/src/entity/routing/routing.ts +++ b/GPTutor-Frontend/src/entity/routing/routing.ts @@ -29,6 +29,7 @@ export enum RoutingPages { gptutorProfile = "/gptutor-profile", mermaidPage = "/mermaid-page", additionalRequest = "/additional-request", + lessonSuggestion = "/lesson-suggestion", mainAnecdote = "/main-anecdote", @@ -68,6 +69,7 @@ export enum Panels { gptutorProfile = "gptutor-profile", mermaidPage = "mermaid-page", additionalRequest = "additional-request", + lessonSuggestion = "lesson-suggestion", gallery = "gallery", diff --git a/GPTutor-Frontend/src/panels/Chapters/Lessons/Lessons.tsx b/GPTutor-Frontend/src/panels/Chapters/Lessons/Lessons.tsx index ad45534e..f3f6d888 100644 --- a/GPTutor-Frontend/src/panels/Chapters/Lessons/Lessons.tsx +++ b/GPTutor-Frontend/src/panels/Chapters/Lessons/Lessons.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from "react"; -import { Placeholder, Search, SimpleCell } from "@vkontakte/vkui"; -import { Icon20ChevronRight, Icon56GhostOutline } from "@vkontakte/icons"; +import { Placeholder, Search, SimpleCell, Div, Button } from "@vkontakte/vkui"; +import { Icon20ChevronRight, Icon56GhostOutline, Icon24AddOutline } from "@vkontakte/icons"; import { ChapterItem, LessonItem } from "$/entity/lessons"; +import { useNavigationContext } from "$/NavigationContext"; import classes from "./Lessons.module.css"; import TertiaryTitle from "$/components/TertiaryTitle"; @@ -15,6 +16,7 @@ interface IProps { function Lessons({ currentChapter, onClickLesson }: IProps) { const lessons = currentChapter.lessons.get(); + const { goToLessonSuggestion } = useNavigationContext(); useEffect(() => { currentChapter.searchLessons(currentChapter.searchValue$.get()); @@ -49,6 +51,17 @@ function Lessons({ currentChapter, onClickLesson }: IProps) { ))} ))} +
+ +
); } diff --git a/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.module.css b/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.module.css new file mode 100644 index 00000000..a91fffd2 --- /dev/null +++ b/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.module.css @@ -0,0 +1,66 @@ +.container { + padding: 16px; +} + +.title { + color: var(--vkui--color_text_primary); + margin-bottom: 16px; +} + +.magicIcon { + color: var(--vkui--color_icon_accent); +} + +.formGroup { + margin-bottom: 16px; +} + +.listContainer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.suggestionCard { + border: 1px solid var(--vkui--color_separator_alpha); + border-radius: 8px; + padding: 16px; + background: var(--vkui--color_background_content); +} + +.suggestionTitle { + font-weight: 600; + color: var(--vkui--color_text_primary); + margin-bottom: 8px; +} + +.suggestionMeta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + color: var(--vkui--color_text_secondary); +} + +.statusBadge { + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.statusPending { + background: var(--vkui--color_background_warning); + color: var(--vkui--color_text_primary); +} + +.statusApproved { + background: var(--vkui--color_background_positive); + color: var(--vkui--color_text_contrast); +} + +.statusRejected { + background: var(--vkui--color_background_negative); + color: var(--vkui--color_text_contrast); +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.tsx b/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.tsx new file mode 100644 index 00000000..27ac8d23 --- /dev/null +++ b/GPTutor-Frontend/src/panels/LessonSuggestion/LessonSuggestion.tsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect } from "react"; +import { + Button, + Div, + FormItem, + FormLayout, + Input, + Panel, + PanelHeaderBack, + PanelHeaderSubmit, + Select, + Spacing, + Textarea, + Title, +} from "@vkontakte/vkui"; +import { + Icon24AddOutline, + Icon28SchoolOutline, +} from "@vkontakte/icons"; + +import { AppPanelHeader } from "$/components/AppPanelHeader"; +import { AppContainer } from "$/components/AppContainer"; +import { useNavigationContext } from "$/NavigationContext"; +import { snackbarNotify } from "$/entity/notify"; +import { + createLessonSuggestion, + getLessonSuggestions, +} from "$/api/lessonSuggestion"; +import { LessonSuggestion as LessonSuggestionType } from "$/entity/lessonSuggestion/types"; + +import classes from "./LessonSuggestion.module.css"; + +interface IProps { + id: string; +} + +function LessonSuggestion({ id }: IProps) { + const { goBack } = useNavigationContext(); + const [suggestions, setSuggestions] = useState([]); + const [isCreating, setIsCreating] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [formData, setFormData] = useState({ + lessonName: "", + description: "", + category: "", + content: "", + }); + + const categories = [ + { label: "JavaScript", value: "javascript" }, + { label: "HTML/CSS", value: "html-css" }, + { label: "React", value: "react" }, + { label: "Git", value: "git" }, + { label: "Python", value: "python" }, + { label: "Другое", value: "other" }, + ]; + + useEffect(() => { + loadSuggestions(); + }, []); + + const loadSuggestions = async () => { + try { + setIsLoading(true); + const data = await getLessonSuggestions(); + setSuggestions(data); + } catch (error) { + snackbarNotify.notify({ + type: "error", + message: "Ошибка при загрузке предложений", + }); + } finally { + setIsLoading(false); + } + }; + + const handleSubmit = async () => { + if (!formData.lessonName.trim() || !formData.description.trim()) { + snackbarNotify.notify({ + type: "error", + message: "Заполните обязательные поля", + }); + return; + } + + try { + setIsLoading(true); + await createLessonSuggestion(formData); + setFormData({ + lessonName: "", + description: "", + category: "", + content: "", + }); + setIsCreating(false); + await loadSuggestions(); + snackbarNotify.notify({ + type: "success", + message: "Предложение урока отправлено!", + }); + } catch (error) { + snackbarNotify.notify({ + type: "error", + message: "Ошибка при отправке предложения", + }); + } finally { + setIsLoading(false); + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "PENDING": + return "На рассмотрении"; + case "APPROVED": + return "Одобрено"; + case "REJECTED": + return "Отклонено"; + default: + return status; + } + }; + + const getStatusClass = (status: string) => { + switch (status) { + case "PENDING": + return classes.statusPending; + case "APPROVED": + return classes.statusApproved; + case "REJECTED": + return classes.statusRejected; + default: + return classes.statusPending; + } + }; + + return ( + + } + after={ + isCreating ? ( + + ) : null + } + > + Предложить урок + + } + > +
+
+ + Предложите урок{" "} + <Icon28SchoolOutline + width={32} + height={32} + className={classes.magicIcon} + /> + + + + {!isCreating ? ( + <> + + + + Ваши предложения + + + {isLoading ? ( +
Загрузка...
+ ) : suggestions.length === 0 ? ( +
У вас пока нет предложений уроков
+ ) : ( +
+ {suggestions.map((suggestion) => ( +
+
+ {suggestion.lessonName} +
+
+ {suggestion.category} + + {getStatusText(suggestion.status)} + +
+
{suggestion.description}
+
+ ))} +
+ )} + + ) : ( + + + + setFormData((prev) => ({ + ...prev, + lessonName: e.target.value, + })) + } + placeholder="Например: Промисы в JavaScript" + /> + + + +