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

400 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="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
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>
.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>