Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add field note to movie table

Revision ID: d6cf1a127f6c
Revises: 5ee6456bd1f5
Create Date: 2026-06-05 20:16:19.752170

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "d6cf1a127f6c"
down_revision: Union[str, Sequence[str], None] = "5ee6456bd1f5"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("movies", sa.Column("note", sa.String(length=50), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("movies", "note")
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions backend/backlog_app/models/movie.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class Movie(Base):
nullable=True,
)

note: Mapped[str | None] = mapped_column(
String(50),
nullable=True,
)

year: Mapped[int | None] = mapped_column(
Integer,
nullable=True,
Expand Down
4 changes: 4 additions & 0 deletions backend/backlog_app/schemas/movie.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class MovieBase(BaseModel):
title: Annotated[str, Len(min_length=3, max_length=255)]
description: Annotated[str, Len(min_length=20, max_length=1000)]
note: Annotated[str, Len(min_length=2, max_length=50)]
year: int
rating: float
watch_link: str | None = None
Expand All @@ -27,13 +28,15 @@ class MovieBase(BaseModel):

class MovieCreate(MovieBase):
description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None
note: Annotated[str, Len(min_length=2, max_length=50)] | None = None
year: int | None = None
rating: float | None = Field(default=None, ge=1.0, le=10.0)


class MovieUpdate(MovieBase):
title: Annotated[str, Len(min_length=3, max_length=255)] | None = None
description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None
note: Annotated[str, Len(min_length=2, max_length=50)] | None = None
year: int | None = None
watched: bool | None = None
rating: float | None = Field(default=None, ge=1.0, le=10.0)
Expand All @@ -43,6 +46,7 @@ class MovieRead(MovieBase):
id: int
user: UserRead
description: Annotated[str, Len(min_length=20, max_length=1000)] | None
note: Annotated[str, Len(min_length=2, max_length=50)] | None = None
year: int | None
watched: bool
rating: float | None
Expand Down
1 change: 1 addition & 0 deletions backend/tests/test_schemas/test_movie.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def test_movie_read_fields_contract():
"id",
"title",
"description",
"note",
"year",
"watch_link",
"rating",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/movies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { UserRead } from './auth'
export interface MovieCreate {
title: string
description?: string | null
note?: string | null
year?: number | null
rating?: number | null
watchLink?: string | null
Expand All @@ -20,6 +21,7 @@ export interface MovieRead {
id: number
title: string
description: string | null
note: string | null
year: number | null
rating: number | null
watchLink: string | null
Expand Down
36 changes: 34 additions & 2 deletions frontend/src/components/ui/AddMovieModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@
placeholder="https://..."
/>

<div class="flex flex-col gap-1.5">
<label class="font-body text-sm font-medium text-base-700">
Заметка
</label>
<input
v-model="form.note"
type="text"
placeholder="Короткая пометка: «пересмотреть», «с женой» и т.д."
maxlength="50"
class="input-field"
:class="{ 'error': errors.note }"
/>
<div class="flex items-center justify-between">
<p v-if="errors.note" class="text-sm text-accent font-body">{{ errors.note }}</p>
</div>
</div>

<div class="flex items-center gap-3">
<BaseToggle v-model="form.published" label="Сделать публичным" />
</div>
Expand Down Expand Up @@ -123,6 +140,7 @@ const emit = defineEmits<{
submit: [data: {
title: string
description: string | null
note: string | null
year: number | null
rating: number | null
watchLink: string | null
Expand All @@ -136,19 +154,21 @@ const loading = ref(false)
const form = reactive({
title: '',
description: '',
note: '',
watchLink: '',
published: false,
watched: false,
})
const yearStr = ref('')
const ratingStr = ref('')
const errors = reactive({ title: '', description: '', year: '', rating: '' })
const errors = reactive({ title: '', description: '', note: '', year: '', rating: '' })

// Populate form when editing
watch(() => props.editMovie, (movie) => {
if (movie) {
form.title = movie.title
form.description = movie.description || ''
form.note = movie.note || ''
form.watchLink = movie.watchLink || ''
form.published = movie.published
form.watched = movie.watched
Expand All @@ -162,12 +182,13 @@ watch(() => props.editMovie, (movie) => {
function resetForm() {
form.title = ''
form.description = ''
form.note = ''
form.watchLink = ''
form.published = false
form.watched = false
yearStr.value = ''
ratingStr.value = ''
Object.assign(errors, { title: '', description: '', year: '', rating: '' })
Object.assign(errors, { title: '', description: '', note: '', year: '', rating: '' })
}

function validate(): boolean {
Expand Down Expand Up @@ -197,6 +218,16 @@ function validate(): boolean {
}
}

if (form.note && form.note.length > 0 && form.note.length < 2) {
errors.note = 'Заметка должна содержать не менее 2 символов'
valid = false
}

if (form.note && form.note.length > 50) {
errors.note = 'Заметка не может быть длиннее 50 символов'
valid = false
}

return valid
}

Expand All @@ -207,6 +238,7 @@ async function handleSubmit() {
emit('submit', {
title: form.title,
description: form.description || null,
note: form.note || null,
year: yearStr.value ? Number(yearStr.value) : null,
rating: ratingStr.value ? Number(ratingStr.value) : null,
watchLink: form.watchLink || null,
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/components/ui/MovieCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

<!-- Main content -->
<div class="p-5 flex-1 flex flex-col">
<RouterLink :to="`/movies/${movie.id}`" class="flex-1">
<RouterLink :to="`/movies/${movie.id}`" class="flex-1 flex flex-col">
<!-- Title + year -->
<div class="flex items-start justify-between gap-3 mb-2">
<h3 class="font-display font-bold text-base-900 group-hover:text-accent transition-colors leading-tight">
Expand All @@ -41,10 +41,18 @@
</div>

<!-- Description -->
<p v-if="movie.description" class="font-body text-xs text-base-400 leading-relaxed line-clamp-3 mb-4">
<p v-if="movie.description" class="font-body text-xs text-base-400 leading-relaxed line-clamp-3 mb-2">
{{ movie.description }}
</p>

<!-- Note -->
<div v-if="movie.note" class="flex items-center gap-1.5 mb-3">
<svg class="w-3 h-3 shrink-0 text-base-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h6m-6 4h10M5 4h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z" />
</svg>
<span class="font-body text-xs text-base-400 italic truncate">{{ movie.note }}</span>
</div>

<!-- Rating -->
<div v-if="movie.rating" class="flex items-center gap-1.5 mb-3">
<div class="flex">
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/views/movies/MovieDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@
<p v-if="movie.description" class="font-body text-base-600 leading-relaxed text-lg">
{{ movie.description }}
</p>

<!-- Note -->
<div v-if="movie.note" class="flex items-start gap-2 mt-4 px-4 py-3 rounded-xl bg-surface-muted border border-surface-border">
<svg class="w-4 h-4 shrink-0 text-base-300 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h6m-6 4h10M5 4h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z" />
</svg>
<span class="font-body text-sm text-base-500 italic">{{ movie.note }}</span>
</div>
</div>

<!-- Meta info -->
Expand Down
Loading