diff --git a/GEMINI.md b/GEMINI.md index c050634..5e13096 100644 --- a/GEMINI.md +++ b/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`). - 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 - **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. diff --git a/app/components/ConfirmDialog.vue b/app/components/ConfirmDialog.vue new file mode 100644 index 0000000..86bb042 --- /dev/null +++ b/app/components/ConfirmDialog.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/app/pages/habits.vue b/app/pages/habits.vue index 910fd72..d1e5df4 100644 --- a/app/pages/habits.vue +++ b/app/pages/habits.vue @@ -1,41 +1,75 @@ @@ -202,6 +334,13 @@ h3 { 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; } @@ -227,4 +366,43 @@ h3 { 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; } diff --git a/app/pages/index.vue b/app/pages/index.vue index ee553e5..76e00dc 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -3,7 +3,7 @@

Ваши цели на сегодня

-

Цели обновляются раз в сутки. Бонусы за выполнение целей усиливаются, если посещать страницу ежедневно!

+

Цели обновляются раз в сутки. Получаемые бонусы усиливаются, если посещать сайт ежедневно.

diff --git a/server/api/habits/[id]/index.delete.ts b/server/api/habits/[id]/index.delete.ts new file mode 100644 index 0000000..bd0602a --- /dev/null +++ b/server/api/habits/[id]/index.delete.ts @@ -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; +}); diff --git a/server/api/habits/[id]/index.put.ts b/server/api/habits/[id]/index.put.ts new file mode 100644 index 0000000..ba1edf1 --- /dev/null +++ b/server/api/habits/[id]/index.put.ts @@ -0,0 +1,56 @@ +import { getUserIdFromSession } from '../../../utils/auth'; + +interface HabitDto { + id: number; + name: string; + daysOfWeek: number[]; +} + +export default defineEventHandler(async (event): Promise => { + 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[], + }; +});