Skip to content

Commit 60743a2

Browse files
committed
working locally
1 parent acb8c09 commit 60743a2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3860
-44
lines changed

README.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Vantage is a full-stack web application that enables teams and friend groups to:
99
- **Discover Movies** - Search and explore films via OMDB API integration with local caching
1010
- **Personal Watchlists** - Track movies you want to watch and mark ones you've seen
1111
- **Rate & Review** - Rate movies on a 5-star scale with your personal collection
12-
- **Movie Clubs** (Coming Soon) - Create clubs with shared watchlists and voting
12+
- **Movie Clubs** - Create clubs with shared watchlists, member management, and voting
1313
- **Watch Parties** (Coming Soon) - Schedule events and coordinate viewing sessions
1414
- **Discussions** (Coming Soon) - Threaded forums for movie discussions
1515

@@ -154,7 +154,8 @@ vantage/
154154
│ │ │ ├── users.py # User management
155155
│ │ │ ├── movies.py # Movie search & details
156156
│ │ │ ├── ratings.py # Movie ratings
157-
│ │ │ └── watchlist.py # Personal watchlist
157+
│ │ │ ├── watchlist.py # Personal watchlist
158+
│ │ │ └── clubs.py # Movie clubs
158159
│ │ ├── core/
159160
│ │ │ ├── config.py # Settings with Pydantic
160161
│ │ │ ├── security.py # Password hashing & JWT
@@ -173,13 +174,16 @@ vantage/
173174
│ │ │ ├── _layout.tsx # Authenticated layout
174175
│ │ │ └── _layout/
175176
│ │ │ ├── index.tsx # Dashboard
177+
│ │ │ ├── clubs.tsx # Movie clubs list
178+
│ │ │ ├── clubs.$clubId.tsx # Club details
176179
│ │ │ ├── movies.tsx # Movie search
177180
│ │ │ ├── movies.$imdbId.tsx # Movie details
178181
│ │ │ ├── watchlist.tsx # Personal watchlist
179182
│ │ │ ├── ratings.tsx # Rating history
180183
│ │ │ └── settings.tsx # User settings
181184
│ │ ├── components/
182185
│ │ │ ├── Movies/ # Movie components
186+
│ │ │ ├── Clubs/ # Club components
183187
│ │ │ ├── Ratings/ # Rating components
184188
│ │ │ ├── ui/ # shadcn/ui components
185189
│ │ │ └── Sidebar/ # Navigation
@@ -262,9 +266,33 @@ vantage/
262266
│ │ id │ │
263267
└───────────┼─ user_id │ │
264268
│ movie_id ───┼──────────────────┘
269+
│ club_id (opt) │
265270
│ score (1-5) │
266271
│ created_at │
267272
└──────────────────┘
273+
274+
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
275+
│ Club │ │ ClubMember │ │ ClubWatchlist │
276+
├──────────────┤ ├──────────────────┤ ├──────────────────┤
277+
│ id │──┐ │ id │ │ id │
278+
│ name │ │ │ club_id ───┼───────┼─ club_id │
279+
│ description │ └────┼─ club_id │ │ movie_id ───┼──► Movie
280+
│ visibility │ │ user_id ───┼──► User │ added_by_user_id │
281+
│ rules │ │ role │ │ notes │
282+
│ theme_color │ │ joined_at │ │ added_at │
283+
│ created_at │ └──────────────────┘ └──────────────────┘
284+
│ updated_at │ │
285+
└──────────────┘ ┌──────────────────────────────────┘
286+
287+
┌───────▼──────────┐
288+
│ ClubWatchlistVote│
289+
├──────────────────┤
290+
│ id │
291+
│ watchlist_entry_id
292+
│ user_id ───┼──► User
293+
│ vote_type │
294+
│ created_at │
295+
└──────────────────┘
268296
```
269297

270298
### API Endpoints
@@ -306,6 +334,23 @@ vantage/
306334
| POST | `/api/v1/ratings/` | Create/update rating |
307335
| DELETE | `/api/v1/ratings/{id}` | Delete rating |
308336

337+
#### Clubs
338+
| Method | Endpoint | Description |
339+
|--------|----------|-------------|
340+
| GET | `/api/v1/clubs/` | List clubs (public + user's clubs) |
341+
| POST | `/api/v1/clubs/` | Create new club |
342+
| GET | `/api/v1/clubs/{club_id}` | Get club with members |
343+
| PATCH | `/api/v1/clubs/{club_id}` | Update club (admin/owner) |
344+
| DELETE | `/api/v1/clubs/{club_id}` | Delete club (owner only) |
345+
| POST | `/api/v1/clubs/{club_id}/join` | Join club |
346+
| DELETE | `/api/v1/clubs/{club_id}/leave` | Leave club |
347+
| PATCH | `/api/v1/clubs/{club_id}/members/{user_id}` | Update member role |
348+
| DELETE | `/api/v1/clubs/{club_id}/members/{user_id}` | Remove member |
349+
| GET | `/api/v1/clubs/{club_id}/watchlist` | Get club watchlist |
350+
| POST | `/api/v1/clubs/{club_id}/watchlist` | Add movie to watchlist |
351+
| DELETE | `/api/v1/clubs/{club_id}/watchlist/{entry_id}` | Remove from watchlist |
352+
| POST | `/api/v1/clubs/{club_id}/watchlist/{entry_id}/vote` | Vote on movie |
353+
309354
## Development
310355

311356
### Database Migrations
@@ -387,7 +432,7 @@ See [deployment.md](deployment.md) for production deployment instructions coveri
387432

388433
- [x] **Phase 1**: OMDB integration, movie search, personal watchlist
389434
- [x] **Phase 2**: Movie ratings, user profiles
390-
- [ ] **Phase 3**: Movie clubs with shared watchlists and voting
435+
- [x] **Phase 3**: Movie clubs with shared watchlists and voting
391436
- [ ] **Phase 4**: Written reviews and discussion forums
392437
- [ ] **Phase 5**: Watch party scheduling and events
393438
- [ ] **Phase 6**: Notifications, activity feeds, achievements

SPEC.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -689,18 +689,18 @@ CREATE INDEX idx_event_club_start ON event(club_id, start_time);
689689

690690
## Development Phases
691691

692-
### Phase 1: Foundation (MVP)
693-
- [ ] OMDB integration (search, cache, display)
694-
- [ ] Movie details page
695-
- [ ] Personal watchlist (want to watch, watched)
696-
- [ ] Basic star ratings
697-
- [ ] Art Deco theme implementation
698-
699-
### Phase 2: Clubs Core
700-
- [ ] Club CRUD operations
701-
- [ ] Club membership (join, leave, invite)
702-
- [ ] Club watchlist with voting
703-
- [ ] Basic club dashboard
692+
### Phase 1: Foundation (MVP)
693+
- [x] OMDB integration (search, cache, display)
694+
- [x] Movie details page
695+
- [x] Personal watchlist (want to watch, watched)
696+
- [x] Basic star ratings
697+
- [x] Art Deco theme implementation
698+
699+
### Phase 2: Clubs Core
700+
- [x] Club CRUD operations
701+
- [x] Club membership (join, leave, role management)
702+
- [x] Club watchlist with voting
703+
- [x] Basic club dashboard
704704

705705
### Phase 3: Social Features
706706
- [ ] Written reviews with spoiler tags
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Add club models
2+
3+
Revision ID: b2c3d4e5f6a7
4+
Revises: a1b2c3d4e5f6
5+
Create Date: 2026-02-04
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "b2c3d4e5f6a7"
14+
down_revision = "a1b2c3d4e5f6"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# Club table
21+
op.create_table(
22+
"club",
23+
sa.Column("id", sa.Uuid(), nullable=False),
24+
sa.Column("name", sa.String(length=100), nullable=False),
25+
sa.Column("description", sa.String(length=1000), nullable=True),
26+
sa.Column("cover_image_url", sa.String(length=1000), nullable=True),
27+
sa.Column("visibility", sa.String(length=20), nullable=False, server_default="public"),
28+
sa.Column("rules", sa.String(length=2000), nullable=True),
29+
sa.Column("theme_color", sa.String(length=20), nullable=True),
30+
sa.Column(
31+
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
32+
),
33+
sa.Column(
34+
"updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
35+
),
36+
sa.PrimaryKeyConstraint("id"),
37+
)
38+
op.create_index("ix_club_name", "club", ["name"])
39+
40+
# ClubMember table
41+
op.create_table(
42+
"club_member",
43+
sa.Column("id", sa.Uuid(), nullable=False),
44+
sa.Column("club_id", sa.Uuid(), nullable=False),
45+
sa.Column("user_id", sa.Uuid(), nullable=False),
46+
sa.Column("role", sa.String(length=20), nullable=False, server_default="member"),
47+
sa.Column(
48+
"joined_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
49+
),
50+
sa.ForeignKeyConstraint(["club_id"], ["club.id"], ondelete="CASCADE"),
51+
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
52+
sa.PrimaryKeyConstraint("id"),
53+
)
54+
op.create_index("ix_club_member_club_id", "club_member", ["club_id"])
55+
op.create_index("ix_club_member_user_id", "club_member", ["user_id"])
56+
op.create_index(
57+
"ix_club_member_club_user", "club_member", ["club_id", "user_id"], unique=True
58+
)
59+
60+
# ClubWatchlist table
61+
op.create_table(
62+
"club_watchlist",
63+
sa.Column("id", sa.Uuid(), nullable=False),
64+
sa.Column("club_id", sa.Uuid(), nullable=False),
65+
sa.Column("movie_id", sa.Uuid(), nullable=False),
66+
sa.Column("added_by_user_id", sa.Uuid(), nullable=False),
67+
sa.Column("notes", sa.String(length=1000), nullable=True),
68+
sa.Column(
69+
"added_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
70+
),
71+
sa.ForeignKeyConstraint(["club_id"], ["club.id"], ondelete="CASCADE"),
72+
sa.ForeignKeyConstraint(["movie_id"], ["movie.id"], ondelete="CASCADE"),
73+
sa.ForeignKeyConstraint(["added_by_user_id"], ["user.id"], ondelete="CASCADE"),
74+
sa.PrimaryKeyConstraint("id"),
75+
)
76+
op.create_index("ix_club_watchlist_club_id", "club_watchlist", ["club_id"])
77+
op.create_index(
78+
"ix_club_watchlist_club_movie", "club_watchlist", ["club_id", "movie_id"], unique=True
79+
)
80+
81+
# ClubWatchlistVote table
82+
op.create_table(
83+
"club_watchlist_vote",
84+
sa.Column("id", sa.Uuid(), nullable=False),
85+
sa.Column("watchlist_entry_id", sa.Uuid(), nullable=False),
86+
sa.Column("user_id", sa.Uuid(), nullable=False),
87+
sa.Column("vote_type", sa.String(length=20), nullable=False),
88+
sa.Column(
89+
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
90+
),
91+
sa.ForeignKeyConstraint(
92+
["watchlist_entry_id"], ["club_watchlist.id"], ondelete="CASCADE"
93+
),
94+
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
95+
sa.PrimaryKeyConstraint("id"),
96+
)
97+
op.create_index("ix_club_watchlist_vote_entry_id", "club_watchlist_vote", ["watchlist_entry_id"])
98+
op.create_index(
99+
"ix_club_watchlist_vote_entry_user",
100+
"club_watchlist_vote",
101+
["watchlist_entry_id", "user_id"],
102+
unique=True,
103+
)
104+
105+
# Add foreign key from rating to club
106+
op.create_foreign_key(
107+
"fk_rating_club_id",
108+
"rating",
109+
"club",
110+
["club_id"],
111+
["id"],
112+
ondelete="SET NULL",
113+
)
114+
115+
116+
def downgrade() -> None:
117+
op.drop_constraint("fk_rating_club_id", "rating", type_="foreignkey")
118+
op.drop_table("club_watchlist_vote")
119+
op.drop_table("club_watchlist")
120+
op.drop_table("club_member")
121+
op.drop_table("club")

backend/app/api/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, movies, private, ratings, users, utils, watchlist
3+
from app.api.routes import clubs, items, login, movies, private, ratings, users, utils, watchlist
44
from app.core.config import settings
55

66
api_router = APIRouter()
@@ -11,6 +11,7 @@
1111
api_router.include_router(movies.router)
1212
api_router.include_router(watchlist.router)
1313
api_router.include_router(ratings.router)
14+
api_router.include_router(clubs.router)
1415

1516

1617
if settings.ENVIRONMENT == "local":

0 commit comments

Comments
 (0)