From 65f21cd0250b650dd7a8653efd7d415c9d2d5914 Mon Sep 17 00:00:00 2001 From: poignatov Date: Fri, 6 Feb 2026 15:16:58 +0300 Subject: [PATCH] =?UTF-8?q?4.24.0:=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=20=D1=81=D0=B1=D1=80=D0=BE=D1=81=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81=D1=81=D0=B0=20=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- play-life-backend/main.go | 125 ++++++++++++++++++++++ play-life-web/package.json | 2 +- play-life-web/src/components/WordList.css | 94 ++++++++++++++++ play-life-web/src/components/WordList.jsx | 82 +++++++++++++- 5 files changed, 302 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 0ef2c60..252fdf2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.23.1 +4.24.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 068eb0e..e9b69d0 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -1543,6 +1543,129 @@ func (a *App) addWordsHandler(w http.ResponseWriter, r *http.Request) { }) } +func (a *App) deleteWordHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + wordID, err := strconv.Atoi(vars["id"]) + if err != nil { + sendErrorWithCORS(w, "Invalid word ID", http.StatusBadRequest) + return + } + + // Verify ownership - check that word belongs to user + var ownerID int + err = a.DB.QueryRow("SELECT user_id FROM words WHERE id = $1", wordID).Scan(&ownerID) + if err == sql.ErrNoRows { + sendErrorWithCORS(w, "Word not found", http.StatusNotFound) + return + } + if err != nil { + log.Printf("Error checking word ownership: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + if ownerID != userID { + sendErrorWithCORS(w, "Word not found", http.StatusNotFound) + return + } + + // Delete the word (progress will be deleted automatically due to CASCADE) + result, err := a.DB.Exec("DELETE FROM words WHERE id = $1", wordID) + if err != nil { + log.Printf("Error deleting word: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + if rowsAffected == 0 { + sendErrorWithCORS(w, "Word not found", http.StatusNotFound) + return + } + + setCORSHeaders(w) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Word deleted successfully", + }) +} + +func (a *App) resetWordProgressHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + wordID, err := strconv.Atoi(vars["id"]) + if err != nil { + sendErrorWithCORS(w, "Invalid word ID", http.StatusBadRequest) + return + } + + // Verify ownership - check that word belongs to user + var ownerID int + err = a.DB.QueryRow("SELECT user_id FROM words WHERE id = $1", wordID).Scan(&ownerID) + if err == sql.ErrNoRows { + sendErrorWithCORS(w, "Word not found", http.StatusNotFound) + return + } + if err != nil { + log.Printf("Error checking word ownership: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + if ownerID != userID { + sendErrorWithCORS(w, "Word not found", http.StatusNotFound) + return + } + + // Reset progress for this word and user + _, err = a.DB.Exec(` + UPDATE progress + SET success = 0, + failure = 0, + last_success_at = NULL, + last_failure_at = NULL + WHERE word_id = $1 AND user_id = $2 + `, wordID, userID) + if err != nil { + log.Printf("Error resetting word progress: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + setCORSHeaders(w) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Word progress reset successfully", + }) +} + func (a *App) getTestWordsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("getTestWordsHandler called: %s %s", r.Method, r.URL.Path) setCORSHeaders(w) @@ -4007,6 +4130,8 @@ func main() { // Words & dictionaries protected.HandleFunc("/api/words", app.getWordsHandler).Methods("GET", "OPTIONS") protected.HandleFunc("/api/words", app.addWordsHandler).Methods("POST", "OPTIONS") + protected.HandleFunc("/api/words/{id}", app.deleteWordHandler).Methods("DELETE", "OPTIONS") + protected.HandleFunc("/api/words/{id}/reset-progress", app.resetWordProgressHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/test/words", app.getTestWordsHandler).Methods("GET", "OPTIONS") protected.HandleFunc("/api/test/progress", app.updateTestProgressHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/dictionaries", app.getDictionariesHandler).Methods("GET", "OPTIONS") diff --git a/play-life-web/package.json b/play-life-web/package.json index 38ebd7e..940c7ab 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.23.1", + "version": "4.24.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/WordList.css b/play-life-web/src/components/WordList.css index 2e2a115..81fea87 100644 --- a/play-life-web/src/components/WordList.css +++ b/play-life-web/src/components/WordList.css @@ -94,6 +94,7 @@ justify-content: space-between; align-items: center; gap: 1.5rem; + cursor: pointer; } .word-card:hover { @@ -248,3 +249,96 @@ transform: scale(1.1); } +.word-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.word-modal { + background: white; + border-radius: 12px; + padding: 0; + max-width: 400px; + width: 90%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + animation: modalSlideIn 0.2s ease-out; +} + +@keyframes modalSlideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.word-modal-header { + display: flex; + justify-content: center; + align-items: center; + padding: 1.5rem 1.5rem 0.5rem 1.5rem; + position: relative; +} + +.word-modal-header h3 { + margin: 0; + color: #2c3e50; + font-size: 1.75rem; + text-align: center; + display: -webkit-box; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.word-modal-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.5rem 1.5rem 1.5rem 1.5rem; +} + +.word-modal-reset, +.word-modal-delete { + width: 100%; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.word-modal-reset { + background-color: #f39c12; + color: white; +} + +.word-modal-reset:hover { + background-color: #e67e22; + transform: translateY(-1px); +} + +.word-modal-delete { + background-color: #e74c3c; + color: white; +} + +.word-modal-delete:hover { + background-color: #c0392b; + transform: translateY(-1px); +} diff --git a/play-life-web/src/components/WordList.jsx b/play-life-web/src/components/WordList.jsx index 825001b..520176f 100644 --- a/play-life-web/src/components/WordList.jsx +++ b/play-life-web/src/components/WordList.jsx @@ -14,6 +14,7 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger = const [dictionaryName, setDictionaryName] = useState('') const [originalDictionaryName, setOriginalDictionaryName] = useState('') const [isSavingName, setIsSavingName] = useState(false) + const [selectedWord, setSelectedWord] = useState(null) // Normalize undefined to null for clarity: new dictionary if dictionaryId is null or undefined const [currentDictionaryId, setCurrentDictionaryId] = useState(dictionaryId ?? null) @@ -92,6 +93,62 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger = } } + const handleWordClick = (word) => { + setSelectedWord(word) + } + + const handleDeleteWord = async () => { + if (!selectedWord) return + + if (!window.confirm('Вы уверены, что хотите удалить это слово?')) { + return + } + + try { + const response = await authFetch(`${API_URL}/words/${selectedWord.id}`, { + method: 'DELETE', + }) + + if (!response.ok) { + throw new Error('Ошибка при удалении слова') + } + + // Remove word from local state + setWords(words.filter(word => word.id !== selectedWord.id)) + setSelectedWord(null) + } catch (err) { + setError(err.message) + } + } + + const handleResetProgress = async () => { + if (!selectedWord) return + + if (!window.confirm('Вы уверены, что хотите сбросить прогресс этого слова?')) { + return + } + + try { + const response = await authFetch(`${API_URL}/words/${selectedWord.id}/reset-progress`, { + method: 'POST', + }) + + if (!response.ok) { + throw new Error('Ошибка при сбросе прогресса') + } + + // Update word in local state - reset progress fields + setWords(words.map(word => + word.id === selectedWord.id + ? { ...word, success: 0, failure: 0, last_success_at: null, last_failure_at: null } + : word + )) + setSelectedWord(null) + } catch (err) { + setError(err.message) + } + } + const handleNameChange = (e) => { setDictionaryName(e.target.value) } @@ -225,7 +282,11 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger = <>
{words.map((word) => ( -
+
handleWordClick(word)} + >

{word.name}

@@ -247,6 +308,25 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger = )} )} + + {/* Модальное окно для действий со словом */} + {selectedWord && ( +
setSelectedWord(null)}> +
e.stopPropagation()}> +
+

{selectedWord.name}

+
+
+ + +
+
+
+ )}
) }