Compare commits
3 Commits
6e9e2db23e
...
8a036df1b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a036df1b4 | ||
|
|
65f21cd025 | ||
|
|
a76d1d40cb |
@@ -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) {
|
func (a *App) getTestWordsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("getTestWordsHandler called: %s %s", r.Method, r.URL.Path)
|
log.Printf("getTestWordsHandler called: %s %s", r.Method, r.URL.Path)
|
||||||
setCORSHeaders(w)
|
setCORSHeaders(w)
|
||||||
@@ -3068,6 +3191,20 @@ func (a *App) getCurrentWeekScores(userID int) (map[int]float64, error) {
|
|||||||
// getTodayScores получает сумму score всех нод, созданных сегодня для конкретного пользователя
|
// getTodayScores получает сумму score всех нод, созданных сегодня для конкретного пользователя
|
||||||
// Возвращает map[project_id]today_score для сегодняшнего дня
|
// Возвращает map[project_id]today_score для сегодняшнего дня
|
||||||
func (a *App) getTodayScores(userID int) (map[int]float64, error) {
|
func (a *App) getTodayScores(userID int) (map[int]float64, error) {
|
||||||
|
// Получаем часовой пояс из переменной окружения (по умолчанию UTC)
|
||||||
|
timezoneStr := getEnv("TIMEZONE", "UTC")
|
||||||
|
loc, err := time.LoadLocation(timezoneStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Invalid timezone '%s': %v. Using UTC instead.", timezoneStr, err)
|
||||||
|
loc = time.UTC
|
||||||
|
timezoneStr = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем текущую дату в нужном часовом поясе
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||||
|
todayEnd := todayStart.Add(24 * time.Hour)
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
n.project_id,
|
n.project_id,
|
||||||
@@ -3078,11 +3215,12 @@ func (a *App) getTodayScores(userID int) (map[int]float64, error) {
|
|||||||
p.deleted = FALSE
|
p.deleted = FALSE
|
||||||
AND p.user_id = $1
|
AND p.user_id = $1
|
||||||
AND n.user_id = $1
|
AND n.user_id = $1
|
||||||
AND DATE(n.created_date) = CURRENT_DATE
|
AND n.created_date >= $2
|
||||||
|
AND n.created_date < $3
|
||||||
GROUP BY n.project_id
|
GROUP BY n.project_id
|
||||||
`
|
`
|
||||||
|
|
||||||
rows, err := a.DB.Query(query, userID)
|
rows, err := a.DB.Query(query, userID, todayStart, todayEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error querying today scores: %v", err)
|
log.Printf("Error querying today scores: %v", err)
|
||||||
return nil, fmt.Errorf("error querying today scores: %w", err)
|
return nil, fmt.Errorf("error querying today scores: %w", err)
|
||||||
@@ -3106,6 +3244,20 @@ func (a *App) getTodayScores(userID int) (map[int]float64, error) {
|
|||||||
// getTodayScoresAllUsers получает сумму score всех нод, созданных сегодня для всех пользователей
|
// getTodayScoresAllUsers получает сумму score всех нод, созданных сегодня для всех пользователей
|
||||||
// Возвращает map[project_id]today_score для сегодняшнего дня
|
// Возвращает map[project_id]today_score для сегодняшнего дня
|
||||||
func (a *App) getTodayScoresAllUsers() (map[int]float64, error) {
|
func (a *App) getTodayScoresAllUsers() (map[int]float64, error) {
|
||||||
|
// Получаем часовой пояс из переменной окружения (по умолчанию UTC)
|
||||||
|
timezoneStr := getEnv("TIMEZONE", "UTC")
|
||||||
|
loc, err := time.LoadLocation(timezoneStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Invalid timezone '%s': %v. Using UTC instead.", timezoneStr, err)
|
||||||
|
loc = time.UTC
|
||||||
|
timezoneStr = "UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем текущую дату в нужном часовом поясе
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||||
|
todayEnd := todayStart.Add(24 * time.Hour)
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
n.project_id,
|
n.project_id,
|
||||||
@@ -3114,11 +3266,12 @@ func (a *App) getTodayScoresAllUsers() (map[int]float64, error) {
|
|||||||
JOIN projects p ON n.project_id = p.id
|
JOIN projects p ON n.project_id = p.id
|
||||||
WHERE
|
WHERE
|
||||||
p.deleted = FALSE
|
p.deleted = FALSE
|
||||||
AND DATE(n.created_date) = CURRENT_DATE
|
AND n.created_date >= $1
|
||||||
|
AND n.created_date < $2
|
||||||
GROUP BY n.project_id
|
GROUP BY n.project_id
|
||||||
`
|
`
|
||||||
|
|
||||||
rows, err := a.DB.Query(query)
|
rows, err := a.DB.Query(query, todayStart, todayEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error querying today scores for all users: %v", err)
|
log.Printf("Error querying today scores for all users: %v", err)
|
||||||
return nil, fmt.Errorf("error querying today scores for all users: %w", err)
|
return nil, fmt.Errorf("error querying today scores for all users: %w", err)
|
||||||
@@ -3977,6 +4130,8 @@ func main() {
|
|||||||
// Words & dictionaries
|
// Words & dictionaries
|
||||||
protected.HandleFunc("/api/words", app.getWordsHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/api/words", app.getWordsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/api/words", app.addWordsHandler).Methods("POST", "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/words", app.getTestWordsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/api/test/progress", app.updateTestProgressHandler).Methods("POST", "OPTIONS")
|
protected.HandleFunc("/api/test/progress", app.updateTestProgressHandler).Methods("POST", "OPTIONS")
|
||||||
protected.HandleFunc("/api/dictionaries", app.getDictionariesHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/api/dictionaries", app.getDictionariesHandler).Methods("GET", "OPTIONS")
|
||||||
@@ -14738,7 +14893,7 @@ func (a *App) getWeeklyStatsDataForUserAndWeek(userID int, year int, week int) (
|
|||||||
p.color
|
p.color
|
||||||
FROM
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
INNER JOIN
|
||||||
weekly_goals wg ON wg.project_id = p.id
|
weekly_goals wg ON wg.project_id = p.id
|
||||||
AND wg.goal_year = $2
|
AND wg.goal_year = $2
|
||||||
AND wg.goal_week = $3
|
AND wg.goal_week = $3
|
||||||
@@ -14749,6 +14904,7 @@ func (a *App) getWeeklyStatsDataForUserAndWeek(userID int, year int, week int) (
|
|||||||
AND $3 = wr.report_week
|
AND $3 = wr.report_week
|
||||||
WHERE
|
WHERE
|
||||||
p.deleted = FALSE AND p.user_id = $1
|
p.deleted = FALSE AND p.user_id = $1
|
||||||
|
AND wg.min_goal_score IS NOT NULL
|
||||||
ORDER BY
|
ORDER BY
|
||||||
total_score DESC
|
total_score DESC
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.23.0",
|
"version": "4.24.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.word-card:hover {
|
.word-card:hover {
|
||||||
@@ -248,3 +249,96 @@
|
|||||||
transform: scale(1.1);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger =
|
|||||||
const [dictionaryName, setDictionaryName] = useState('')
|
const [dictionaryName, setDictionaryName] = useState('')
|
||||||
const [originalDictionaryName, setOriginalDictionaryName] = useState('')
|
const [originalDictionaryName, setOriginalDictionaryName] = useState('')
|
||||||
const [isSavingName, setIsSavingName] = useState(false)
|
const [isSavingName, setIsSavingName] = useState(false)
|
||||||
|
const [selectedWord, setSelectedWord] = useState(null)
|
||||||
// Normalize undefined to null for clarity: new dictionary if dictionaryId is null or undefined
|
// Normalize undefined to null for clarity: new dictionary if dictionaryId is null or undefined
|
||||||
const [currentDictionaryId, setCurrentDictionaryId] = useState(dictionaryId ?? null)
|
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) => {
|
const handleNameChange = (e) => {
|
||||||
setDictionaryName(e.target.value)
|
setDictionaryName(e.target.value)
|
||||||
}
|
}
|
||||||
@@ -225,7 +282,11 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger =
|
|||||||
<>
|
<>
|
||||||
<div className="words-grid">
|
<div className="words-grid">
|
||||||
{words.map((word) => (
|
{words.map((word) => (
|
||||||
<div key={word.id} className="word-card">
|
<div
|
||||||
|
key={word.id}
|
||||||
|
className="word-card"
|
||||||
|
onClick={() => handleWordClick(word)}
|
||||||
|
>
|
||||||
<div className="word-content">
|
<div className="word-content">
|
||||||
<div className="word-header">
|
<div className="word-header">
|
||||||
<h3 className="word-name">{word.name}</h3>
|
<h3 className="word-name">{word.name}</h3>
|
||||||
@@ -247,6 +308,25 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger =
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно для действий со словом */}
|
||||||
|
{selectedWord && (
|
||||||
|
<div className="word-modal-overlay" onClick={() => setSelectedWord(null)}>
|
||||||
|
<div className="word-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="word-modal-header">
|
||||||
|
<h3>{selectedWord.name}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="word-modal-actions">
|
||||||
|
<button className="word-modal-reset" onClick={handleResetProgress}>
|
||||||
|
Сбросить
|
||||||
|
</button>
|
||||||
|
<button className="word-modal-delete" onClick={handleDeleteWord}>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user