Loading...
;
+ if (isLoading) return Loading...
;
+ if(error) {
+ navigate('/not-found');
+ }
return actor ? (
@@ -116,3 +104,5 @@ export default function ActorPage() {
);
}
+
+
diff --git a/app/src/pages/AdvancedSearch.tsx b/app/src/pages/AdvancedSearch.tsx
index 880c3a3..7d9f73e 100644
--- a/app/src/pages/AdvancedSearch.tsx
+++ b/app/src/pages/AdvancedSearch.tsx
@@ -19,15 +19,22 @@ import { Link } from 'react-router-dom';
import { FaSearch } from 'react-icons/fa';
import { MdStar } from 'react-icons/md';
import { handleSearchAll } from '@/lib/MovieService';
+import { SearchAllData } from '@/lib/dataconnect-sdk';
const genres = ['', 'action', 'crime', 'drama', 'sci-fi', 'thriller', 'adventure'];
+interface SearchResults {
+moviesMatchingTitle?: SearchAllData['moviesMatchingTitle'];
+ moviesMatchingDescription?: SearchAllData['moviesMatchingDescription']
+ actors?: SearchAllData['actorsMatchingName'];
+ reviews?: SearchAllData['reviewsMatchingText'];
+}
export default function AdvancedSearchPage() {
const [searchQuery, setSearchQuery] = useState('');
const [releaseYearRange, setReleaseYearRange] = useState({ min: 1900, max: 2030 });
const [genre, setGenre] = useState('');
const [ratingRange, setRatingRange] = useState({ min: 1, max: 10 });
- const [results, setResults] = useState({
+ const [results, setResults] = useState
({
moviesMatchingTitle: [],
moviesMatchingDescription: [],
actors: [],
diff --git a/app/src/pages/Home.tsx b/app/src/pages/Home.tsx
index 0bb83e6..b13c2ed 100644
--- a/app/src/pages/Home.tsx
+++ b/app/src/pages/Home.tsx
@@ -14,30 +14,25 @@
* limitations under the License.
*/
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import Carousel from '@/components/carousel';
-import { handleGetTopMovies, handleGetLatestMovies } from '@/lib/MovieService';
+import { OrderDirection } from '@/lib/dataconnect-sdk';
+import { useHandleLatestMovies, useHandleTopMovies } from '@/lib/MovieService';
export default function HomePage() {
- const [topMovies, setTopMovies] = useState([]);
- const [latestMovies, setLatestMovies] = useState([]);
+ const { data: topMoviesData, isLoading: topMoviesLoading } = useHandleTopMovies(10, OrderDirection.DESC );
+ const { data: latestMoviesData, isLoading: latestMoviesLoading } = useHandleLatestMovies(10, OrderDirection.DESC);
- useEffect(() => {
- async function fetchMovies() {
- const topMoviesData = await handleGetTopMovies(10);
- const latestMoviesData = await handleGetLatestMovies(10);
-
- if (topMoviesData) setTopMovies(topMoviesData);
- if (latestMoviesData) setLatestMovies(latestMoviesData);
- }
-
- fetchMovies();
- }, []);
+ if(topMoviesLoading || latestMoviesLoading) {
+ return Loading...
+ }
return (
-
-
+
+
);
}
+
+
diff --git a/app/src/pages/Movie.tsx b/app/src/pages/Movie.tsx
index c4186e2..7265726 100644
--- a/app/src/pages/Movie.tsx
+++ b/app/src/pages/Movie.tsx
@@ -21,36 +21,60 @@ import { onAuthStateChanged, User } from "firebase/auth";
import { AuthContext } from "@/lib/firebase";
import NotFound from "./NotFound";
import {
- handleGetMovieById,
- handleGetIfFavoritedMovie,
- handleAddFavoritedMovie,
- handleDeleteFavoritedMovie,
- handleAddReview,
- handleDeleteReview,
fetchSimilarMovies,
+ useHandleAddFavoritedMovie,
+ useHandleAddReview,
+ useHandleDeleteFavoritedMovie,
+ useHandleDeleteReview,
+ useHandleGetIfFavoritedMovie,
+ useHandleGetMovieById,
} from "@/lib/MovieService";
import MovieCard from "@/components/moviecard";
+import {
+ DateString,
+ User_Key,
+ UUIDString,
+} from "@/lib/dataconnect-sdk";
+
+interface UserReview {
+ id: UUIDString;
+ reviewText?: string | null;
+ reviewDate: DateString;
+ rating?: number | null;
+ user: {
+ id: string;
+ username: string;
+ } & User_Key;
+}
export default function MoviePage() {
const { id } = useParams() as { id: string };
const auth = useContext(AuthContext);
- const [loading, setLoading] = useState(true);
const [authUser, setAuthUser] = useState(null);
- const [isFavorited, setIsFavorited] = useState(false);
const [reviewText, setReviewText] = useState("");
const [rating, setRating] = useState(0);
- const [movie, setMovie] = useState(null);
- const [userReview, setUserReview] = useState(null);
- const [similarMovies, setSimilarMovies] = useState([]);
+ const [userReview, setUserReview] = useState(null);
+ const [similarMovies, setSimilarMovies] = useState([]);
+
+ const { data, isLoading, error } = useHandleGetMovieById(id);
+ const movie = data?.movie;
+ const { mutate: handleAddFavoritedMovie } = useHandleAddFavoritedMovie(id);
+ const { mutate: handleDeleteFavoritedMovie } = useHandleDeleteFavoritedMovie(id);
+ const { mutate: handleAddReview } = useHandleAddReview(id);
+ const { mutate: handleDeleteReview } = useHandleDeleteReview();
+ const { data: favoritedMovieData } = useHandleGetIfFavoritedMovie(
+ id ,
+ !!authUser
+ );
+ const isFavorited = !!favoritedMovieData?.favorite_movie;
// Fetch the movie details and check if it's favorited when the user is authenticated
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setAuthUser(user);
- handleGetIfFavoritedMovie(id).then(setIsFavorited);
}
});
@@ -60,25 +84,22 @@ export default function MoviePage() {
// Fetch movie details and the user's review
useEffect(() => {
if (id) {
- handleGetMovieById(id).then((movieData) => {
- setMovie(movieData);
- if (movieData?.reviews) {
- const userReview = movieData.reviews.find(
+ if (movie) {
+ if (movie.reviews) {
+ const userReview = movie.reviews.find(
(review) => review.user.id === authUser?.uid
);
- fetchSimilarMovies(movieData.description).then((similarMovies) => {
+ fetchSimilarMovies(movie.description!).then((similarMovies) => {
const similarResults = similarMovies?.filter(
- (movie) => movie.id !== movieData.id
+ (movie) => movie.id !== movie.id
);
setSimilarMovies(
similarResults && similarResults.length > 1 ? similarResults : []
);
- setMovie(movieData);
});
setUserReview(userReview || null);
}
- setLoading(false);
- });
+ }
}
}, [id, authUser]);
@@ -90,11 +111,10 @@ export default function MoviePage() {
try {
if (isFavorited) {
- await handleDeleteFavoritedMovie(id);
+ await handleDeleteFavoritedMovie({ movieId: id });
} else {
- await handleAddFavoritedMovie(id);
+ await handleAddFavoritedMovie({ movieId: id });
}
- setIsFavorited(!isFavorited);
} catch (error) {
console.error("Error updating favorite status:", error);
}
@@ -106,11 +126,11 @@ export default function MoviePage() {
if (!authUser) return;
try {
- await handleAddReview(id, rating, reviewText);
+ await handleAddReview({ movieId: id, rating, reviewText });
setReviewText("");
setRating(0);
- const updatedMovie = await handleGetMovieById(id);
- setMovie(updatedMovie);
+ // const updatedMovie = await handleGetMovieById(id);
+ // setMovie(updatedMovie);
} catch (error) {
console.error("Error submitting review:", error);
}
@@ -123,17 +143,15 @@ export default function MoviePage() {
if (!authUser || !userReview) return;
try {
- await handleDeleteReview(id);
+ await handleDeleteReview({ movieId: id });
setUserReview(null);
- const updatedMovie = await handleGetMovieById(id);
- setMovie(updatedMovie);
} catch (error) {
console.error("Error deleting review:", error);
}
};
- if (loading) return Loading...
;
- if (!movie) return ;
+ if (isLoading) return Loading...
;
+ if (error || !movie) return ;
return (
@@ -301,3 +319,5 @@ export default function MoviePage() {
);
}
+
+
diff --git a/app/src/pages/MyProfile.tsx b/app/src/pages/MyProfile.tsx
index ac9741c..0a6ccb6 100644
--- a/app/src/pages/MyProfile.tsx
+++ b/app/src/pages/MyProfile.tsx
@@ -17,7 +17,7 @@
import React, { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { onAuthStateChanged, User } from "firebase/auth";
-import { handleGetCurrentUser, handleDeleteReview } from "@/lib/MovieService";
+import { useHandleDeleteReview, useHandleGetCurrentUser} from "@/lib/MovieService";
import { MdStar } from "react-icons/md";
import { AuthContext } from "@/lib/firebase";
import MovieCard from "@/components/moviecard";
@@ -25,16 +25,15 @@ import MovieCard from "@/components/moviecard";
export default function MyProfilePage() {
const navigate = useNavigate();
const [authUser, setAuthUser] = useState(null);
- const [loading, setLoading] = useState(true);
const auth = useContext(AuthContext);
- const [user, setUser] = useState(null);
-
+ const {mutate: handleDeleteReview } = useHandleDeleteReview();
+ const { data: userData, isLoading, refetch } = useHandleGetCurrentUser(!!authUser);
+ const user = userData?.user;
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setAuthUser(user);
- loadUserProfile();
} else {
navigate("/");
}
@@ -43,28 +42,18 @@ export default function MyProfilePage() {
return () => unsubscribe();
}, [navigate, auth]);
- async function loadUserProfile() {
- try {
- const userProfile = await handleGetCurrentUser();
- setUser(userProfile);
- } catch (error) {
- console.error("Error loading user profile:", error);
- } finally {
- setLoading(false);
- }
- }
async function deleteReview(reviewMovieId: string) {
if (!authUser) return;
try {
- await handleDeleteReview(reviewMovieId);
- loadUserProfile();
+ await handleDeleteReview({movieId: reviewMovieId});
+ refetch();
} catch (error) {
console.error("Error deleting review:", error);
}
}
- if (loading) return Loading...
;
+ if (isLoading) return Loading...
;
if (!user) return User not found.
;
return (
diff --git a/app/src/pages/VectorSearch.tsx b/app/src/pages/VectorSearch.tsx
index 8a1a5c4..988e304 100644
--- a/app/src/pages/VectorSearch.tsx
+++ b/app/src/pages/VectorSearch.tsx
@@ -22,7 +22,7 @@ export default function VectorSearchPage() {
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
- const [results, setResults] = useState([]);
+ const [results, setResults] = useState([]);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
diff --git a/app/tsconfig.json b/app/tsconfig.json
index 7c235bb..4ddeb3c 100644
--- a/app/tsconfig.json
+++ b/app/tsconfig.json
@@ -17,8 +17,9 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
+ "strictNullChecks": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
diff --git a/dataconnect/movie-connector/connector.yaml b/dataconnect/movie-connector/connector.yaml
index a214dfe..c8f74c1 100644
--- a/dataconnect/movie-connector/connector.yaml
+++ b/dataconnect/movie-connector/connector.yaml
@@ -18,3 +18,4 @@ generate:
package: "@movie/dataconnect"
outputDir: ../../app/src/lib/dataconnect-sdk
packageJsonDir: "../../app"
+ react: true
diff --git a/dataconnect/movie-connector/mutations.gql b/dataconnect/movie-connector/mutations.gql
index e8c3e63..f4acd09 100644
--- a/dataconnect/movie-connector/mutations.gql
+++ b/dataconnect/movie-connector/mutations.gql
@@ -15,19 +15,44 @@
# Upsert (update or insert) a user based on their username
# TODO: Complete UpsertUser
+# Create or update the current authenticated user
+mutation UpsertUser($username: String!) @auth(level: USER) {
+ user_upsert(
+ data: {
+ id_expr: "auth.uid"
+ username: $username
+ }
+ )
+}
# Add a movie to the user's favorites list
-# TODO: Complete AddFavoritedMovie
+mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
+ favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
+}
# Remove a movie from the user's favorites list
-# TODO: Complete DeleteFavoritedMovie
+mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
+ favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
+}
# Add a review for a movie
-# TODO: Complete AddReview
+mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
+@auth(level: USER) {
+ review_insert(
+ data: {
+ userId_expr: "auth.uid"
+ movieId: $movieId
+ rating: $rating
+ reviewText: $reviewText
+ reviewDate_date: { today: true }
+ }
+ )
+}
# Delete a user's review for a movie
-# TODO: Complete DeleteReview
-
+mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
+ review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
+}
# The mutations below are unused by the application, but are useful examples for more complex cases
diff --git a/dataconnect/movie-connector/queries.gql b/dataconnect/movie-connector/queries.gql
index c05306d..b6c644a 100644
--- a/dataconnect/movie-connector/queries.gql
+++ b/dataconnect/movie-connector/queries.gql
@@ -15,33 +15,204 @@
# List subset of fields for movies
# TODO: Update ListMovies
-# query ListMovies @auth(level: PUBLIC) {
-# movies{
-# id
-# title
-# imageUrl
-# releaseYear
-# genre
-# rating
-# tags
-# description
-# }
-# }
+#
+# List subset of fields for movies
+query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC) {
+ movies(
+ orderBy: [
+ { rating: $orderByRating },
+ { releaseYear: $orderByReleaseYear }
+ ]
+ limit: $limit
+ ) {
+ id
+ title
+ imageUrl
+ releaseYear
+ genre
+ rating
+ tags
+ description
+ }
+}
# Get movie by id
# TODO: Complete GetMovieById
+# Get movie by id
+query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
+movie(id: $id) {
+ id
+ title
+ imageUrl
+ releaseYear
+ genre
+ rating
+ description
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ name
+ imageUrl
+ }
+ supportingActors: actors_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ name
+ imageUrl
+ }
+ reviews: reviews_on_movie {
+ id
+ reviewText
+ reviewDate
+ rating
+ user {
+ id
+ username
+ }
+ }
+ }
+}
# Get actor by id
# TODO: Complete GetActorById
+query GetActorById($id: UUID!) @auth(level: PUBLIC) {
+ actor(id: $id) {
+ id
+ name
+ imageUrl
+ mainActors: movies_via_MovieActor(where: { role: { eq: "main" } }) {
+ id
+ title
+ genre
+ tags
+ imageUrl
+ }
+ supportingActors: movies_via_MovieActor(
+ where: { role: { eq: "supporting" } }
+ ) {
+ id
+ title
+ genre
+ tags
+ imageUrl
+ }
+ }
+}
+
+
# Get current authenticated user
-# TODO: Complete GetCurrentUser
+query GetCurrentUser @auth(level: USER) {
+ user(key: { id_expr: "auth.uid" }) {
+ id
+ username
+ reviews: reviews_on_user {
+ id
+ rating
+ reviewDate
+ reviewText
+ movie {
+ id
+ title
+ }
+ }
+ favoriteMovies: favorite_movies_on_user {
+ movie {
+ id
+ title
+ genre
+ imageUrl
+ releaseYear
+ rating
+ description
+ tags
+ metadata: movieMetadatas_on_movie {
+ director
+ }
+ }
+ }
+ }
+}
-# Check if a movie is favorited by user
-# TODO: Complete GetIfFavoritedMovie
+query GetIfFavoritedMovie($movieId: UUID!) @auth(level: USER) {
+ favorite_movie(key: { userId_expr: "auth.uid", movieId: $movieId }) {
+ movieId
+ }
+}
# Search for movies, actors, and reviews
# TODO: Complete SearchAll
+# Search for movies, actors, and reviews
+query SearchAll(
+ $input: String
+ $minYear: Int!
+ $maxYear: Int!
+ $minRating: Float!
+ $maxRating: Float!
+ $genre: String!
+) @auth(level: PUBLIC) {
+ moviesMatchingTitle: movies(
+ where: {
+ _and: [
+ { releaseYear: { ge: $minYear } }
+ { releaseYear: { le: $maxYear } }
+ { rating: { ge: $minRating } }
+ { rating: { le: $maxRating } }
+ { genre: { contains: $genre } }
+ { title: { contains: $input } }
+ ]
+ }
+ ) {
+ id
+ title
+ genre
+ rating
+ imageUrl
+ tags
+ }
+ moviesMatchingDescription: movies(
+ where: {
+ _and: [
+ { releaseYear: { ge: $minYear } }
+ { releaseYear: { le: $maxYear } }
+ { rating: { ge: $minRating } }
+ { rating: { le: $maxRating } }
+ { genre: { contains: $genre } }
+ { description: { contains: $input } }
+ ]
+ }
+ ) {
+ id
+ title
+ genre
+ rating
+ imageUrl
+ tags
+ }
+ actorsMatchingName: actors(where: { name: { contains: $input } }) {
+ id
+ name
+ imageUrl
+ }
+ reviewsMatchingText: reviews(where: { reviewText: { contains: $input } }) {
+ id
+ rating
+ reviewText
+ reviewDate
+ movie {
+ id
+ title
+ }
+ user {
+ id
+ username
+ }
+ }
+}
# Search movie descriptions using L2 similarity with Vertex AI
# TODO: Complete SearchMovieDescriptionUsingL2Similarity
diff --git a/dataconnect/schema/schema.gql b/dataconnect/schema/schema.gql
index 6dac321..7d8b594 100644
--- a/dataconnect/schema/schema.gql
+++ b/dataconnect/schema/schema.gql
@@ -14,29 +14,86 @@
# Movies
# TODO: Fill out Movie table
+type Movie
+ @table {
+ id: UUID! @default(expr: "uuidV4()")
+ title: String!
+ imageUrl: String!
+ releaseYear: Int
+ genre: String
+ rating: Float
+ description: String
+ tags: [String]
+}
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
# TODO: Fill out MovieMetadata table
+type MovieMetadata
+ @table {
+ # @ref creates a field in the current table (MovieMetadata)
+ # It is a reference that holds the primary key of the referenced type
+ # In this case, @ref(fields: "movieId", references: "id") is implied
+ movie: Movie! @ref
+ # movieId: UUID <- this is created by the above @ref
+ director: String
+}
# Actors
# Suppose an actor can participate in multiple movies and movies can have multiple actors
# Movie - Actors (or vice versa) is a many to many relationship
# TODO: Fill out Actor table
+type Actor @table {
+ id: UUID!
+ imageUrl: String!
+ name: String! @col(name: "name", dataType: "varchar(30)")
+}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]
# TODO: Fill out MovieActor table
+type MovieActor @table(key: ["movie", "actor"]) {
+ # @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type
+ # In this case, @ref(fields: "id") is implied
+ movie: Movie!
+ # movieId: UUID! <- this is created by the implied @ref, see: implicit.gql
+
+ actor: Actor!
+ # actorId: UUID! <- this is created by the implied @ref, see: implicit.gql
+
+ role: String! # "main" or "supporting"
+}
# Users
# Suppose a user can leave reviews for movies
# user-reviews is a one to many relationship, movie-reviews is a one to many relationship, movie:user is a many to many relationship
# TODO: Fill out User table
+type User
+ @table {
+ id: String! @col(name: "auth_uid")
+ username: String! @col(dataType: "varchar(50)")
+ # The following are generated from the @ref in the Review table
+ # reviews_on_user
+ # movies_via_Review
+}
# Join table for many-to-many relationship for users and favorite movies
# TODO: Fill out FavoriteMovie table
+type FavoriteMovie
+ @table(name: "FavoriteMovies", singular: "favorite_movie", plural: "favorite_movies", key: ["user", "movie"]) {
+ # @ref is implicit
+ user: User!
+ movie: Movie!
+}
# Reviews
# TODO: Fill out Review table
-
+type Review @table(name: "Reviews", key: ["movie", "user"]) {
+ id: UUID! @default(expr: "uuidV4()")
+ user: User!
+ movie: Movie!
+ rating: Int
+ reviewText: String
+ reviewDate: Date! @default(expr: "request.time")
+}