редактирование и удаление привычек
This commit is contained in:
parent
b2b1ba078e
commit
8e1d026fd4
10
GEMINI.md
10
GEMINI.md
|
|
@ -69,6 +69,16 @@ Adhering to these conventions is critical for maintaining project stability.
|
||||||
- Group API endpoints by domain (e.g., `/api/habits`, `/api/village`).
|
- Group API endpoints by domain (e.g., `/api/habits`, `/api/village`).
|
||||||
- All business logic should reside in the backend.
|
- All business logic should reside in the backend.
|
||||||
|
|
||||||
|
### Data Conventions
|
||||||
|
- **`daysOfWeek` Field:** This array represents the days a habit is active. The week starts on **Monday**.
|
||||||
|
- Monday: `0`
|
||||||
|
- Tuesday: `1`
|
||||||
|
- Wednesday: `2`
|
||||||
|
- Thursday: `3`
|
||||||
|
- Friday: `4`
|
||||||
|
- Saturday: `5`
|
||||||
|
- Sunday: `6`
|
||||||
|
|
||||||
### AI / Gemini Usage Rules
|
### AI / Gemini Usage Rules
|
||||||
- **DO NOT** allow the AI to change the Node.js version, upgrade Prisma, alter the Prisma configuration, or modify the core project structure.
|
- **DO NOT** allow the AI to change the Node.js version, upgrade Prisma, alter the Prisma configuration, or modify the core project structure.
|
||||||
- **ALLOWED:** The AI can be used to add or modify Prisma schema models, generate new API endpoints, and implement business logic within the existing architectural framework.
|
- **ALLOWED:** The AI can be used to add or modify Prisma schema models, generate new API endpoints, and implement business logic within the existing architectural framework.
|
||||||
|
|
|
||||||
94
app/components/ConfirmDialog.vue
Normal file
94
app/components/ConfirmDialog.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="show" class="modal-overlay" @click.self="$emit('cancel')">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button @click="$emit('cancel')" class="btn-cancel">{{ cancelText }}</button>
|
||||||
|
<button @click="$emit('confirm')" class="btn-confirm">{{ confirmText }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Подтверждение',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
confirmText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Да',
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Отмена',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['confirm', 'cancel']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background-color: #bf616a; /* Nord Red for destructive actions */
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background-color: #e5e9f0; /* Nord light gray */
|
||||||
|
color: #4c566a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,41 +1,75 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="habits-container">
|
<div class="habits-container">
|
||||||
<h3>My Habits</h3>
|
<h3>Мои Привычки</h3>
|
||||||
|
|
||||||
<!-- Create Habit Form -->
|
<!-- Create Habit Form -->
|
||||||
<form @submit.prevent="createHabit" class="create-habit-form">
|
<form @submit.prevent="createHabit" class="create-habit-form">
|
||||||
<h4>Create a New Habit</h4>
|
<h4>Создать новую привычку</h4>
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
<input v-model="newHabitName" type="text" placeholder="e.g., Read for 15 minutes" required />
|
<input v-model="newHabitName" type="text" placeholder="Например, читать 15 минут" required />
|
||||||
<div class="days-selector">
|
<div class="days-selector">
|
||||||
<label v-for="day in dayOptions" :key="day" class="day-label">
|
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
||||||
<input type="checkbox" :value="day" v-model="newHabitDays" />
|
<input type="checkbox" :value="day.name" v-model="newHabitDays" />
|
||||||
<span>{{ day }}</span>
|
<span>{{ day.name }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" :disabled="loading.create">
|
<button type="submit" :disabled="loading.create">
|
||||||
{{ loading.create ? 'Adding...' : 'Add Habit' }}
|
{{ loading.create ? 'Добавляем...' : 'Добавить Привычку' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Habits List -->
|
<!-- Habits List -->
|
||||||
<div class="habits-list">
|
<div class="habits-list">
|
||||||
<p v-if="loading.fetch">Loading habits...</p>
|
<p v-if="loading.fetch">Загрузка привычек...</p>
|
||||||
<div v-for="habit in habits" :key="habit.id" class="habit-card">
|
<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">
|
<div class="habit-info">
|
||||||
<h4>{{ habit.name }}</h4>
|
<h4>{{ habit.name }}</h4>
|
||||||
<div class="habit-days">
|
<div class="habit-days">
|
||||||
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ dayMap[day] }}</span>
|
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ dayMap[day] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<p v-if="!loading.fetch && habits.length === 0">No habits yet. Add one above!</p>
|
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import ConfirmDialog from '~/components/ConfirmDialog.vue';
|
||||||
|
|
||||||
// --- Type Definitions ---
|
// --- Type Definitions ---
|
||||||
interface Habit {
|
interface Habit {
|
||||||
|
|
@ -50,15 +84,36 @@ const api = useApi();
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const habits = ref<Habit[]>([]);
|
const habits = ref<Habit[]>([]);
|
||||||
const newHabitName = ref('');
|
const newHabitName = ref('');
|
||||||
const dayOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
// Day mapping based on Mon=0, ..., Sun=6
|
||||||
const dayMap: { [key: number]: string } = { 0: 'Mon', 1: 'Tue', 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' };
|
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 newHabitDays = ref<string[]>([]);
|
||||||
|
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const loading = ref({
|
const loading = ref({
|
||||||
fetch: false,
|
fetch: false,
|
||||||
create: 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 ---
|
// --- API Functions ---
|
||||||
const fetchHabits = async () => {
|
const fetchHabits = async () => {
|
||||||
loading.value.fetch = true;
|
loading.value.fetch = true;
|
||||||
|
|
@ -67,7 +122,7 @@ const fetchHabits = async () => {
|
||||||
habits.value = await api<Habit[]>('/habits');
|
habits.value = await api<Habit[]>('/habits');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch habits:', err);
|
console.error('Failed to fetch habits:', err);
|
||||||
error.value = 'Could not load habits.';
|
error.value = 'Не удалось загрузить привычки.';
|
||||||
} finally {
|
} finally {
|
||||||
loading.value.fetch = false;
|
loading.value.fetch = false;
|
||||||
}
|
}
|
||||||
|
|
@ -75,17 +130,16 @@ const fetchHabits = async () => {
|
||||||
|
|
||||||
const createHabit = async () => {
|
const createHabit = async () => {
|
||||||
if (!newHabitName.value || newHabitDays.value.length === 0) {
|
if (!newHabitName.value || newHabitDays.value.length === 0) {
|
||||||
error.value = 'Please provide a name and select at least one day.';
|
error.value = 'Пожалуйста, укажите название и выберите хотя бы один день.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value.create = true;
|
loading.value.create = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
// Convert day names to numbers
|
const dayNumbers = newHabitDays.value.map(dayName => dayOptions.find(d => d.name === dayName)!.value);
|
||||||
const dayNumbers = newHabitDays.value.map(dayName => dayOptions.indexOf(dayName));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api<Habit>('/habits', {
|
const newHabit = await api<Habit>('/habits', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
name: newHabitName.value,
|
name: newHabitName.value,
|
||||||
|
|
@ -93,19 +147,97 @@ const createHabit = async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear form and re-fetch the list from the server
|
habits.value.push(newHabit); // Optimistic update
|
||||||
newHabitName.value = '';
|
newHabitName.value = '';
|
||||||
newHabitDays.value = [];
|
newHabitDays.value = [];
|
||||||
await fetchHabits();
|
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to create habit:', err);
|
console.error('Failed to create habit:', err);
|
||||||
error.value = err.data?.message || 'Could not create habit.';
|
error.value = err.data?.message || 'Не удалось создать привычку.';
|
||||||
|
// Re-fetch on error to ensure consistency
|
||||||
|
await fetchHabits();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value.create = false;
|
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 ---
|
// --- Lifecycle Hooks ---
|
||||||
onMounted(fetchHabits);
|
onMounted(fetchHabits);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -202,6 +334,13 @@ h3 {
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
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 {
|
.habit-info h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
@ -227,4 +366,43 @@ h3 {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
text-align: center;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div v-if="isAuthenticated && user" class="dashboard-content">
|
<div v-if="isAuthenticated && user" class="dashboard-content">
|
||||||
|
|
||||||
<h1>Ваши цели на сегодня</h1>
|
<h1>Ваши цели на сегодня</h1>
|
||||||
<p>Цели обновляются раз в сутки. Бонусы за выполнение целей усиливаются, если посещать страницу ежедневно!</p>
|
<p>Цели обновляются раз в сутки. Получаемые бонусы усиливаются, если посещать сайт ежедневно.</p>
|
||||||
|
|
||||||
<div class="streak-section">
|
<div class="streak-section">
|
||||||
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 1 }">
|
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 1 }">
|
||||||
|
|
|
||||||
36
server/api/habits/[id]/index.delete.ts
Normal file
36
server/api/habits/[id]/index.delete.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { getUserIdFromSession } from '../../../utils/auth';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const userId = await getUserIdFromSession(event);
|
||||||
|
const habitId = Number(event.context.params?.id);
|
||||||
|
|
||||||
|
if (isNaN(habitId)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid habit ID.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Authorization & Deletion ---
|
||||||
|
// First, verify the habit exists and belongs to the user.
|
||||||
|
const habit = await prisma.habit.findUnique({
|
||||||
|
where: {
|
||||||
|
id: habitId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!habit || habit.userId !== userId) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Habit not found or permission denied.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, delete the habit
|
||||||
|
await prisma.habit.delete({
|
||||||
|
where: {
|
||||||
|
id: habitId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Response ---
|
||||||
|
// Send 204 No Content status
|
||||||
|
setResponseStatus(event, 204);
|
||||||
|
|
||||||
|
// Return null or an empty body
|
||||||
|
return null;
|
||||||
|
});
|
||||||
56
server/api/habits/[id]/index.put.ts
Normal file
56
server/api/habits/[id]/index.put.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { getUserIdFromSession } from '../../../utils/auth';
|
||||||
|
|
||||||
|
interface HabitDto {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
daysOfWeek: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event): Promise<HabitDto> => {
|
||||||
|
const userId = await getUserIdFromSession(event);
|
||||||
|
const habitId = Number(event.context.params?.id);
|
||||||
|
const { name, daysOfWeek } = await readBody(event);
|
||||||
|
|
||||||
|
if (isNaN(habitId)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid habit ID.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validation ---
|
||||||
|
if (!name || !Array.isArray(daysOfWeek)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid input: name and daysOfWeek are required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize daysOfWeek to ensure it's a unique set of valid numbers
|
||||||
|
const validDays = daysOfWeek.filter(day => typeof day === 'number' && day >= 0 && day <= 6);
|
||||||
|
const sanitizedDays = [...new Set(validDays)].sort();
|
||||||
|
|
||||||
|
// --- Authorization & Update ---
|
||||||
|
// First, verify the habit exists and belongs to the user.
|
||||||
|
const habit = await prisma.habit.findUnique({
|
||||||
|
where: {
|
||||||
|
id: habitId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!habit || habit.userId !== userId) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Habit not found or permission denied.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, update the habit
|
||||||
|
const updatedHabit = await prisma.habit.update({
|
||||||
|
where: {
|
||||||
|
id: habitId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
daysOfWeek: sanitizedDays,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return DTO
|
||||||
|
return {
|
||||||
|
id: updatedHabit.id,
|
||||||
|
name: updatedHabit.name,
|
||||||
|
daysOfWeek: updatedHabit.daysOfWeek as number[],
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user