habits.andr33v.ru/app/pages/habits.vue

409 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="habits-container">
<h3>Мои Привычки</h3>
<!-- Create Habit Form -->
<form @submit.prevent="createHabit" class="create-habit-form">
<h4>Создать новую привычку</h4>
<div v-if="error" class="error-message">{{ error }}</div>
<input v-model="newHabitName" type="text" placeholder="Например, читать 15 минут" required />
<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>
<button type="submit" :disabled="loading.create">
{{ loading.create ? 'Добавляем...' : 'Добавить Привычку' }}
</button>
</form>
<!-- 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">
<div class="habit-info">
<h4>{{ habit.name }}</h4>
<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="edit-btn">Редактировать</button>
<button @click="promptForDelete(habit.id)" :disabled="loading.delete" class="delete-btn">Удалить</button>
</div>
</div>
<!-- Editing Mode -->
<form v-else @submit.prevent="saveHabit(habit.id)" class="habit-edit-form">
<input v-model="editHabitName" type="text" required />
<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 class="edit-actions">
<button type="submit" :disabled="loading.edit" class="save-btn">Сохранить</button>
<button type="button" @click="cancelEditing" class="cancel-btn">Отмена</button>
</div>
</form>
</div>
<p v-if="!loading.fetch && habits.length === 0">Пока нет привычек. Добавьте одну!</p>
</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';
import ConfirmDialog from '~/components/ConfirmDialog.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
await fetchHabits();
} 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>
.habits-container {
max-width: 600px;
margin: 0 auto;
padding-bottom: 40px;
}
h3 {
text-align: center;
margin-bottom: 20px;
}
/* Create Form */
.create-habit-form {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
margin-bottom: 30px;
}
.create-habit-form h4 {
margin-top: 0;
text-align: center;
}
.create-habit-form input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 15px;
}
.days-selector {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.day-label {
cursor: pointer;
text-align: center;
}
.day-label input {
display: none;
}
.day-label span {
display: inline-block;
width: 35px;
line-height: 35px;
border: 1px solid #ccc;
border-radius: 50%;
font-size: 0.9em;
}
.day-label input:checked + span {
background-color: #81a1c1;
color: white;
border-color: #81a1c1;
}
.create-habit-form button {
width: 100%;
padding: 10px;
background-color: #5e81ac;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
/* Habits List */
.habits-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.habit-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.habit-view {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.habit-info h4 {
margin: 0 0 10px 0;
}
.habit-days {
display: flex;
gap: 5px;
}
.day-chip {
background-color: #eceff4;
color: #4c566a;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.8em;
}
.error-message {
color: #bf616a;
background-color: #fbe2e5;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
text-align: center;
}
/* Actions */
.habit-actions button {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
.edit-btn { background-color: #d8dee9; color: #4c566a; }
.delete-btn { background-color: #bf616a; color: #fff; }
/* Edit Form */
.habit-edit-form {
width: 100%;
}
.habit-edit-form input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
.edit-days {
margin-bottom: 10px;
}
.edit-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.edit-actions button {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.save-btn { background-color: #a3be8c; color: #fff; }
.cancel-btn { background-color: #eceff4; color: #4c566a; }
</style>