399 lines
10 KiB
Vue
399 lines
10 KiB
Vue
<template>
|
||
<div class="page-container">
|
||
<h1>Мои Привычки</h1>
|
||
|
||
|
||
<!-- Habits List -->
|
||
<div class="habits-list">
|
||
<p v-if="loading.fetch">Загрузка привычек...</p>
|
||
<div v-for="habit in habits" :key="habit.id" class="habit-card">
|
||
<!-- Viewing Mode -->
|
||
<div v-if="editingHabitId !== habit.id" class="habit-view-content">
|
||
<div class="habit-info">
|
||
<h3>{{ habit.name }}</h3>
|
||
<div class="habit-days">
|
||
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ dayMap[day] }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="habit-actions">
|
||
<button @click="startEditing(habit)" class="btn btn-secondary btn-sm">Редактировать</button>
|
||
<button @click="promptForDelete(habit.id)" :disabled="loading.delete" class="btn btn-danger btn-sm">Удалить</button>
|
||
</div>
|
||
</div>
|
||
<!-- Editing Mode -->
|
||
<form v-else @submit.prevent="saveHabit(habit.id)" class="habit-edit-form">
|
||
<div class="form-group">
|
||
<input v-model="editHabitName" type="text" class="form-control" required />
|
||
</div>
|
||
<div class="form-group">
|
||
<div class="days-selector edit-days">
|
||
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
||
<input type="checkbox" :value="day.name" v-model="editHabitDays" />
|
||
<span>{{ day.name }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="edit-actions">
|
||
<button type="submit" :disabled="loading.edit" class="btn btn-primary btn-sm">Сохранить</button>
|
||
<button type="button" @click="cancelEditing" class="btn btn-secondary btn-sm">Отмена</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<p v-if="!loading.fetch && habits.length === 0">Пока нет привычек. Добавьте одну!</p>
|
||
</div>
|
||
|
||
<!-- Create Habit Form -->
|
||
<div class="form-container">
|
||
<h2>Новая привычка</h2>
|
||
<form @submit.prevent="createHabit">
|
||
<div v-if="error" class="error-message">{{ error }}</div>
|
||
|
||
<div class="form-group">
|
||
<label for="newHabitName" class="form-label">Название привычки</label>
|
||
<input id="newHabitName" v-model="newHabitName" type="text" placeholder="Например, читать 15 минут" class="form-control" required />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Дни недели</label>
|
||
<div class="days-selector">
|
||
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
||
<input type="checkbox" :value="day.name" v-model="newHabitDays" />
|
||
<span>{{ day.name }}</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" :disabled="loading.create" class="btn btn-primary">
|
||
{{ loading.create ? 'Добавляем...' : 'Добавить Привычку' }}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
|
||
<!-- Deletion Confirmation Dialog -->
|
||
<ConfirmDialog
|
||
:show="isConfirmDialogOpen"
|
||
title="Подтвердить удаление"
|
||
message="Вы уверены, что хотите удалить эту привычку? Это действие нельзя отменить."
|
||
confirm-text="Удалить"
|
||
cancel-text="Отмена"
|
||
@confirm="handleDeleteConfirm"
|
||
@cancel="handleDeleteCancel"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue';
|
||
|
||
// --- Type Definitions ---
|
||
interface Habit {
|
||
id: number;
|
||
name: string;
|
||
daysOfWeek: number[]; // Backend returns numbers
|
||
}
|
||
|
||
// --- Composables ---
|
||
const api = useApi();
|
||
|
||
// --- State ---
|
||
const habits = ref<Habit[]>([]);
|
||
const newHabitName = ref('');
|
||
// Day mapping based on Mon=0, ..., Sun=6
|
||
const dayOptions = [
|
||
{ name: 'Пн', value: 0 },
|
||
{ name: 'Вт', value: 1 },
|
||
{ name: 'Ср', value: 2 },
|
||
{ name: 'Чт', value: 3 },
|
||
{ name: 'Пт', value: 4 },
|
||
{ name: 'Сб', value: 5 },
|
||
{ name: 'Вс', value: 6 },
|
||
];
|
||
const dayMap: { [key: number]: string } = Object.fromEntries(dayOptions.map(d => [d.value, d.name]));
|
||
const newHabitDays = ref<string[]>([]);
|
||
|
||
const error = ref<string | null>(null);
|
||
const loading = ref({
|
||
fetch: false,
|
||
create: false,
|
||
edit: false,
|
||
delete: false,
|
||
});
|
||
|
||
// Editing state
|
||
const editingHabitId = ref<number | null>(null);
|
||
const editHabitName = ref('');
|
||
const editHabitDays = ref<string[]>([]);
|
||
|
||
// Deletion state
|
||
const isConfirmDialogOpen = ref(false);
|
||
const habitToDeleteId = ref<number | null>(null);
|
||
|
||
// --- API Functions ---
|
||
const fetchHabits = async () => {
|
||
loading.value.fetch = true;
|
||
error.value = null;
|
||
try {
|
||
habits.value = await api<Habit[]>('/habits');
|
||
} catch (err: any) {
|
||
console.error('Failed to fetch habits:', err);
|
||
error.value = 'Не удалось загрузить привычки.';
|
||
} finally {
|
||
loading.value.fetch = false;
|
||
}
|
||
};
|
||
|
||
const createHabit = async () => {
|
||
if (!newHabitName.value || newHabitDays.value.length === 0) {
|
||
error.value = 'Пожалуйста, укажите название и выберите хотя бы один день.';
|
||
return;
|
||
}
|
||
loading.value.create = true;
|
||
error.value = null;
|
||
|
||
const dayNumbers = newHabitDays.value.map(dayName => dayOptions.find(d => d.name === dayName)!.value);
|
||
|
||
try {
|
||
const newHabit = await api<Habit>('/habits', {
|
||
method: 'POST',
|
||
body: {
|
||
name: newHabitName.value,
|
||
daysOfWeek: dayNumbers,
|
||
},
|
||
});
|
||
|
||
habits.value.push(newHabit); // Optimistic update
|
||
newHabitName.value = '';
|
||
newHabitDays.value = [];
|
||
|
||
} catch (err: any) {
|
||
console.error('Failed to create habit:', err);
|
||
error.value = err.data?.message || 'Не удалось создать привычку.';
|
||
// Re-fetch on error to ensure consistency
|
||
} finally {
|
||
loading.value.create = false;
|
||
}
|
||
};
|
||
|
||
const startEditing = (habit: Habit) => {
|
||
editingHabitId.value = habit.id;
|
||
editHabitName.value = habit.name;
|
||
editHabitDays.value = habit.daysOfWeek.map(dayValue => dayMap[dayValue]);
|
||
};
|
||
|
||
const cancelEditing = () => {
|
||
editingHabitId.value = null;
|
||
editHabitName.value = '';
|
||
editHabitDays.value = [];
|
||
};
|
||
|
||
const saveHabit = async (habitId: number) => {
|
||
loading.value.edit = true;
|
||
error.value = null;
|
||
|
||
const dayNumbers = editHabitDays.value.map(dayName => dayOptions.find(d => d.name === dayName)!.value);
|
||
|
||
try {
|
||
const updatedHabit = await api<Habit>(`/habits/${habitId}`, {
|
||
method: 'PUT',
|
||
body: {
|
||
name: editHabitName.value,
|
||
daysOfWeek: dayNumbers,
|
||
},
|
||
});
|
||
|
||
// Update local state
|
||
const index = habits.value.findIndex(h => h.id === habitId);
|
||
if (index !== -1) {
|
||
habits.value[index] = updatedHabit;
|
||
}
|
||
cancelEditing();
|
||
|
||
} catch (err: any) {
|
||
console.error('Failed to save habit:', err);
|
||
error.value = err.data?.message || 'Не удалось сохранить привычку.';
|
||
} finally {
|
||
loading.value.edit = false;
|
||
}
|
||
};
|
||
|
||
const promptForDelete = (habitId: number) => {
|
||
habitToDeleteId.value = habitId;
|
||
isConfirmDialogOpen.value = true;
|
||
};
|
||
|
||
const handleDeleteCancel = () => {
|
||
isConfirmDialogOpen.value = false;
|
||
habitToDeleteId.value = null;
|
||
};
|
||
|
||
const handleDeleteConfirm = () => {
|
||
if (habitToDeleteId.value !== null) {
|
||
deleteHabit(habitToDeleteId.value);
|
||
}
|
||
handleDeleteCancel();
|
||
};
|
||
|
||
const deleteHabit = async (habitId: number) => {
|
||
loading.value.delete = true;
|
||
error.value = null;
|
||
try {
|
||
await api(`/habits/${habitId}`, { method: 'DELETE' });
|
||
|
||
// Update local state
|
||
habits.value = habits.value.filter(h => h.id !== habitId);
|
||
|
||
} catch (err: any) {
|
||
console.error('Failed to delete habit:', err);
|
||
error.value = err.data?.message || 'Не удалось удалить привычку.';
|
||
} finally {
|
||
loading.value.delete = false;
|
||
}
|
||
};
|
||
|
||
|
||
// --- Lifecycle Hooks ---
|
||
onMounted(fetchHabits);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.form-container {
|
||
background-color: var(--container-bg-color);
|
||
padding: 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
margin-bottom: 32px;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.days-selector {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.day-label {
|
||
cursor: pointer;
|
||
text-align: center;
|
||
}
|
||
|
||
.day-label input {
|
||
display: none;
|
||
}
|
||
|
||
.day-label span {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 33px;
|
||
height: 33px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 50%;
|
||
font-size: 0.9em;
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
.day-label input:checked + span {
|
||
background-color: var(--primary-color);
|
||
color: white;
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.form-container button {
|
||
width: 100%;
|
||
}
|
||
|
||
|
||
/* Habits List */
|
||
.habits-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
margin-bottom: 45px;
|
||
}
|
||
|
||
.habit-card {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
background-color: var(--container-bg-color);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border-color);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.habit-view-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px; /* Spacing between name, days, and actions */
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.habit-info h3 {
|
||
margin: 0;
|
||
font-size: 1.15rem;
|
||
}
|
||
|
||
.habit-days {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
justify-content: flex-start;
|
||
padding: 15px 0 5px;
|
||
}
|
||
|
||
.day-chip {
|
||
background-color: #e5e7eb;
|
||
color: #4b5563;
|
||
padding: 4px 10px;
|
||
border-radius: 16px;
|
||
font-size: 0.8em;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.error-message {
|
||
color: var(--danger-color);
|
||
background-color: #fee2e2;
|
||
padding: 1rem;
|
||
border-radius: 0.375rem;
|
||
margin-bottom: 1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.habit-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
}
|
||
|
||
/* Edit Form Specific Styles */
|
||
.habit-edit-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
gap: 16px;
|
||
}
|
||
|
||
.habit-edit-form .form-group {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.habit-edit-form .days-selector {
|
||
justify-content: flex-start;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.edit-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
}
|
||
</style>
|
||
|