こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、GraphQLを使ってTMDBから映画情報(画像など)を取得し、ページ上に表示する処理を実装したいと思います。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
<目次>
前回
GraphQLでSearch機能を実装しました。
事前準備
フォルダ/ファイル
1 2 |
touch .env touch go-movies/src/components/OneMovieGraphQL.tsx |
The Movie Database(TMDB)
TMDBは、無料で使えるMovieデータを扱うAPIです。
※ただし、利用状況によっては課金される場合もあり、使い方に注意が必要
Sign upしなければ使えるようになりませんが、結構便利です。
画面右上にあるユーザーアカウントから「Settings」->「API」へと進んでいただき、利用規約に同意して個人情報を入力後にAPIキーを取得することができます。

取得したAPIキーは大事なものなので、外部に流出しないように気をつけましょう!
.envファイルにAPIキーを記述します。
1 |
TMDB_KEY=XXXXXXXXXXXXXXXX |
docker-composeファイルは、コンテナ起動時に.envファイルをデフォルトで読み込んでくれます。
その為、environmentフィールドに設定しておけば、コンテナ内で環境変数としてしようできます。
今回は、TMDB_KEYという名前で環境変数を登録しておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# docker-compose.yml version: "3.3" services: backend: build: context: . dockerfile: ./Dockerfile-golang ports: - 4000:4000 environment: - TMDB_KEY: $TMDB_KEY volumes: - ./backend-app:/app |
OneMovieGraphqlコンポーネントの実装
以前、OneMovieコンポーネントを実装ましたが、それのGraphQL版を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// components/OneMovieGraphQL.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovieGraphQL = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [] }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { const payload = ` { movie(id: ${props.match.params.id}) { id title runtime year description release_data rating mpaa_rating } } ` graphQLRequest(payload) }, []) const graphQLRequest = async (payload: string) => { const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql', payload, config) .then((response) => { setMovie(response.data.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovieGraphQL |
ルートを追加
Appコンポーネントにルートを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/App.tsx ... import OneMovieGraphQL from './components/OneMovieGraphQL' const App = (props: any) => { ... return ( <Router> ... <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/moviesgraphql/:id" component={OneMovieGraphQL} /> // 追加 ... </Switch> </div> </div> </div> </Router> ) } ... |
GraphQLコンポーネントの修正
GraphQLコンポーネントからOneMovieGraphQLコンポーネントへ遷移できるように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// components/GraphQL.tsx ... const GraphQL = (props: any) => { ... return ( <Fragment> <h2>GraphQL</h2> <hr /> <Input title={"Search"} type={"text"} name={"search"} value={searchTerm} handleChange={setSearchTerm} /> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/moviesgraphql/${m.id}`} className="list-group-item list-group-item-action" > <strong>{m.title}</strong> <br /> <small className="text-muted"> ({m.year}) - {m.runtime} minutes </small> <br /> {m.description.slice(0, 100)} </Link> ) })} </div> </Fragment> ) } export default GraphQL |
ついでに、Movieの詳細情報(descriptionなど)が表示されるようにも変更しました。
検証
早速、検証してみましょう。
OKですね。
Posterカラムの追加
MovieテーブルにPosterカラムを追加します。
DB操作
コンテナを立ち上げた状態で「http://localhost:9232/」にアクセスしてください。

Moviesの「SQL Query」タブに移動して、下記のコマンドを実行(Run Query)します。
1 2 |
# posterカラムの追加 ALTER TABLE movies ADD COLUMN poster character varying; |

クエリが問題なく発行されれば、moviesテーブルにposterカラムが追加されるはずです。
Movieモデルの修正
GoのMovieモデルにposterを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// models/models.go ... // Movie is the type for movies type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` MovieGenre map[int]string `json:"genres"` Poster string `json:"poster"` } |
CreatedAtとUpdatedAtのJsonタグも「-」から変更しています。
Queryの変更
Posterを追加したので、Queryも変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// models/movies-db.go func (m *DBModel) All(genre ...int) ([]*Movie, error) { ... query := fmt.Sprintf( `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at, coalesce(poster, '') from movies %s order by title`, // poster追加 where) ... for rows.Next() { var movie Movie err := rows.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, &movie.Poster, // 追加 ) ... } } func (m *DBModel) Get(id int) (*Movie, error) { ... query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at, coalesce(poster, '') from movies where id = $1` // poster追加 ... err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, &movie.Poster, // 追加 ) ... } func (m *DBModel) InsertMovie(movie Movie) error { ... stmt := `insert movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` // $10追加 _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, movie.Poster, // 追加 ) ... } func (m *DBModel) UpdateMovie(movie Movie) error { ... stmt := `update movies set title = $1, description = $2, year = $3, release_date = $4, runtime = $5, rating = $6, mpaa_rating = $7, updated_at = $8, poster = $9 where id = $10` // 追加 _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.UpdatedAt, movie.Poster, // 追加、CreatedAtは削除 movie.ID, ) ... } |
GraphQLスキーマーの変更
GraphQLのスキーマーにPosterを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// cmd/api/graphql.go ... var ( ... movieType = graphql.NewObject( graphql.ObjectConfig{ Name: "Movie", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "title": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, "year": &graphql.Field{ Type: graphql.Int, }, "release_date": &graphql.Field{ Type: graphql.DateTime, }, "runtime": &graphql.Field{ Type: graphql.Int, }, "rating": &graphql.Field{ Type: graphql.Int, }, "mpaa_rating": &graphql.Field{ Type: graphql.String, }, "created_at": &graphql.Field{ Type: graphql.DateTime, }, "updated_at": &graphql.Field{ Type: graphql.DateTime, }, "poster": &graphql.Field{ // 追加 Type: graphql.String, }, }, }, ) ) |
TMDB APIへのリクエスト
先に取得したTMDBのAPI Keyを使って、TMDBへのリクエスト処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
// cmd/api/movie-handlers.go func getPoster(movie models.Movie) models.Movie { // TMDBのスキーマーに合わせて構造体を作成 type TheMovieDB struct { Page int `json:"page"` Results []struct { Adult bool `json:"adult"` BackdropPath string `json:"backdrop_path"` GenreIds []int `json:"genre_ids"` ID int `json:"id"` OriginalLanguage string `json:"original_language"` OriginalTitle string `json:"original_title"` Overview string `json:"overview"` Popularity float64 `json:"popularity"` PosterPath string `json:"poster_path"` ReleaseDate string `json:"release_date"` Title string `json:"title"` Video bool `json:"video"` VoteAverage float64 `json:"vote_average"` VoteCount int `json:"vote_count"` } `json:"results"` TotalPages int `json:"total_pages"` TotalResults int `json:"total_results"` } client := &http.Client{} key := os.Getenv("TMDB_KEY") if key == "" { log.Fatal("Should set up TMDB Key") } theUrl := "https://api.themoviedb.org/3/search/movie?api_key=" log.Println(theUrl + key + "&query=" + url.QueryEscape(movie.Title)) req, err := http.NewRequest("GET", theUrl+key+"&query="+url.QueryEscape(movie.Title), nil) if err != nil { log.Println(err) return movie } req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { log.Println(err) return movie } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Println(err) return movie } var responseObject TheMovieDB json.Unmarshal(bodyBytes, &responseObject) if len(responseObject.Results) > 0 { movie.Poster = responseObject.Results[0].PosterPath } return movie } |
API Keyは非公開にしたいので、.envファイルにTMDB_KEYを設定し、それをdocker-composeファイルに読み込ませています。
※docker-composeはデフォルトで.envファイルを読み込む仕様になってます。
そして、osパッケージのGetenvメソッドにてTMDB KEYを取り出しています。
あとはそれを使って、HTTPリクエストをTMDB APIへ投げるだけです。
このメソッドは、editMovieメソッドから呼び出しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<meta charset="utf-8">// cmd/api/movie-handlers.go func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { ... movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime = payload.RunTime movie.Rating = payload.Rating movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() if movie.Poster == "" { movie = getPoster(movie) } ... } |
フロントエンドの修正
posterを表示できるようにフロントエンド(React)側を修正します。
Movieモデル
Movieモデルにposterを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// models/movie.ts export interface Movie { id: number title: string description: string year: string release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> poster: string } |
EditMovieコンポーネントの修正
Movieモデルの修正により、EditMovieコンポーネントも修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: Number(runtime), rating: Number(rating), mpaa_rating: mpaaRating, genres: [], poster: "", // 追加 } ... } |
OneMovieコンポーネントの修正
OneMovieコンポーネントも修正します。
1 2 3 4 5 6 7 8 9 10 |
// components/OneMovie.tsx ... const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [], poster: '', // 追加 }) ... } |
OneMovieGraphQLコンポーネントの修正
先程実装したOneMovieGraphQLコンポーネントにposterを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// components/OneMovieGraphQL.tsx ... const OneMovieGraphQL = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [], poster: '', / 追加 }) ... useEffect(() => { const payload = ` { movie(id: ${props.match.params.id}) { id title runtime year description release_date rating mpaa_rating poster // 追加 } } ` graphQLRequest(payload) }, []) ... return ( <Fragment> ... {movie.poster !== "" && ( <div> <img src={`http://image.tmdb.org/t/p/w200${movie.poster}`} alt="poster" /> </div> )} ... </Fragment> ) } export default OneMovieGraphQL |
image画像を表示されるようにしました。
検証
検証してみましょう。
「http://localhost:3000/moviesgraphql/4」にアクセスしてみてください。

画像が表示されませんね。しかし、これは正常です。
DBに画像情報が格納されていないためです。

そこで、データを登録し直してみます。
まずは、下記のユーザーでログインします。
- me@here.com
- password
それから「http://localhost:3000/admin/movie/4」に移動していただいて、項目を何も変更せずに「Save」ボタンを押下しましょう。
再び、DBを確認するとposterに画像が格納されたことがわかります。

この状態であれば、「http://localhost:3000/moviesgraphql/4」から画像を確認できます。
まとめ
長かったMovie App開発もこれでお終いです。
Dockerの開発環境構築、Golangでのバックグラウンドの開発、Reactでのフロントエンドの開発、PostgreSQL操作、GraphQL操作など色々やりましたね。
ここで学んだことは実務でも役立つと思うので、ぜひ積極的に取り入れてみてください!
それでは、また!
記事まとめ
参考書籍
ソースコード
graphql.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
// cmd/api/graphql.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strings" "github.com/graphql-go/graphql" ) var ( movies []*models.Movie // graphql schema definition fields = graphql.Fields{ "movie": &graphql.Field{ Type: movieType, Description: "Get movie by id", Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.Int, }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(int) if ok { for _, movie := range movies { if movie.ID == id { return movie, nil } } } return nil, nil }, }, "list": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Get all movies", Resolve: func(params graphql.ResolveParams) (interface{}, error) { return movies, nil }, }, "search": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Search movies by title", Args: graphql.FieldConfigArgument{ "titleContains": &graphql.ArgumentConfig{ Type: graphql.String, }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { var theList []*models.Movie search, ok := params.Args["titleContains"].(string) if ok { for _, currentMovie := range movies { if strings.Contains(currentMovie.Title, search) { log.Println("Fond one") theList = append(theList, currentMovie) } } } return theList, nil }, }, } movieType = graphql.NewObject( graphql.ObjectConfig{ Name: "Movie", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "title": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, "year": &graphql.Field{ Type: graphql.Int, }, "release_date": &graphql.Field{ Type: graphql.DateTime, }, "runtime": &graphql.Field{ Type: graphql.Int, }, "rating": &graphql.Field{ Type: graphql.Int, }, "mpaa_rating": &graphql.Field{ Type: graphql.String, }, "created_at": &graphql.Field{ Type: graphql.DateTime, }, "updated_at": &graphql.Field{ Type: graphql.DateTime, }, "poster": &graphql.Field{ Type: graphql.String, }, }, }, ) ) func (app *application) moviesGraphQL(w http.ResponseWriter, r *http.Request) { movies, _ = app.models.DB.All() q, _ := io.ReadAll(r.Body) query := string(q) log.Println(query) rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} schema, err := graphql.NewSchema(schemaConfig) if err != nil { app.errorJSON(w, errors.New("failed to create schema")) log.Println(err) return } params := graphql.Params{Schema: schema, RequestString: query} resp := graphql.Do(params) if len(resp.Errors) > 0 { app.errorJSON(w, errors.New(fmt.Sprintf("failed: %+v", resp.Errors))) } j, _ := json.MarshalIndent(resp, "", " ") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(j) } |
movie-handlers.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
// cmd/api/movie-handlers.go package main import ( "backend/models" "encoding/json" "errors" "io" "log" "net/http" "net/url" "os" "strconv" "time" "github.com/julienschmidt/httprouter" ) func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) // urlからidを抜きだす id, err := strconv.Atoi(params.ByName("id")) if err != nil { app.logger.Print(errors.New("invalid id parameter")) app.errorJSON(w, err) return } movie, err := app.models.DB.Get(id) err = app.writeJSON(w, http.StatusOK, movie, "movie") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllMovie(w http.ResponseWriter, r *http.Request) { movies, err := app.models.DB.All() if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, movies, "movies") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllGenres(w http.ResponseWriter, r *http.Request) { genres, err := app.models.DB.GenresAll() if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, genres, "genres") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllMoviesByGenres(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) genreID, err := strconv.Atoi(params.ByName("genre_id")) if err != nil { app.errorJSON(w, err) return } movies, err := app.models.DB.All(genreID) if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, movies, "movies") if err != nil { app.errorJSON(w, err) return } } type MoviePayload struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"Description"` Year string `json:"year"` ReleaseDate string `json:"release_date"` RunTime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` } type jsonResp struct { OK bool `json:"ok"` Message string `json:"message"` } func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { var payload MoviePayload err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { log.Println(err) app.errorJSON(w, err) return } var movie models.Movie if payload.ID != "0" { id, _ := strconv.Atoi(payload.ID) // idからMovieデータを取得 m, _ := app.models.DB.Get(id) // ポインタを渡してm == movieにする movie = *m movie.UpdatedAt = time.Now() } movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime = payload.RunTime movie.Rating = payload.Rating movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() if movie.Poster == "" { movie = getPoster(movie) } if movie.ID == 0 { err = app.models.DB.InsertMovie(movie) if err != nil { app.errorJSON(w, err) return } } else { err = app.models.DB.UpdateMovie(movie) if err != nil { app.errorJSON(w, err) return } } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } func (app *application) deleteMovie(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) id, err := strconv.Atoi(params.ByName("id")) if err != nil { app.errorJSON(w, err) return } err = app.models.DB.DeleteMovie(id) if err != nil { app.errorJSON(w, err) return } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "reponse") if err != nil { app.errorJSON(w, err) return } } func getPoster(movie models.Movie) models.Movie { // TMDBのスキーマーに合わせて構造体を作成 type TheMovieDB struct { Page int `json:"page"` Results []struct { Adult bool `json:"adult"` BackdropPath string `json:"backdrop_path"` GenreIds []int `json:"genre_ids"` ID int `json:"id"` OriginalLanguage string `json:"original_language"` OriginalTitle string `json:"original_title"` Overview string `json:"overview"` Popularity float64 `json:"popularity"` PosterPath string `json:"poster_path"` ReleaseDate string `json:"release_date"` Title string `json:"title"` Video bool `json:"video"` VoteAverage float64 `json:"vote_average"` VoteCount int `json:"vote_count"` } `json:"results"` TotalPages int `json:"total_pages"` TotalResults int `json:"total_results"` } client := &http.Client{} key := os.Getenv("TMDB_KEY") if key == "" { log.Fatal("Should set up TMDB Key") } theUrl := "https://api.themoviedb.org/3/search/movie?api_key=" log.Println(theUrl + key + "&query=" + url.QueryEscape(movie.Title)) req, err := http.NewRequest("GET", theUrl+key+"&query="+url.QueryEscape(movie.Title), nil) if err != nil { log.Println(err) return movie } req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { log.Println(err) return movie } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { log.Println(err) return movie } var responseObject TheMovieDB json.Unmarshal(bodyBytes, &responseObject) if len(responseObject.Results) > 0 { movie.Poster = responseObject.Results[0].PosterPath } return movie } |
models.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// models/models.go package models import ( "database/sql" "time" ) // Models is the wrapper for database type Models struct { DB DBModel } // NewModels returns models with db pool func NewModels(db *sql.DB) Models { return Models{ DB: DBModel{DB: db}, } } // Movie is the type for movies type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` MovieGenre map[int]string `json:"genres"` Poster string `json:"poster"` } // Genre is the type for genre type Genre struct { // ID int `json:"-"` ID int `json:"id"` GenreName string `json:"genre_name"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } // MovieGenre is thee type for movie genre type MovieGenre struct { ID int `json:"-"` MovieID string `json:"-"` GenreID string `json:"-` Genre Genre `json:"genre"` CreatedAt time.Time `json:"-"` UpdateAt time.Time `json:"-"` } // User is the type for users type User struct { ID int Email string Password string } |
movies-db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
// models/movies-db.go package models import ( "context" "database/sql" "fmt" "time" ) type DBModel struct { DB *sql.DB } // Get retuns one movie and error, if any func (m *DBModel) Get(id int) (*Movie, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at, coalesce(poster, '') from movies where id = $1` row := m.DB.QueryRowContext(ctx, query, id) var movie Movie err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, &movie.Poster, ) if err != nil { return nil, err } // get genres, if any query = `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1` rows, _ := m.DB.QueryContext(ctx, query, id) defer rows.Close() genres := make(map[int]string) for rows.Next() { var mg MovieGenre err := rows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } movie.MovieGenre = genres return &movie, nil } // All retuns all movies and error, if any func (m *DBModel) All(genre ...int) ([]*Movie, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() where := "" if len(genre) > 0 { where = fmt.Sprintf( "where id in (select movie_id from movies_genres where genre_id = %d)", genre[0]) } query := fmt.Sprintf( `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at, coalesce(poster, '') from movies %s order by title`, where) rows, err := m.DB.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var movies []*Movie for rows.Next() { var movie Movie err := rows.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, &movie.Poster, ) if err != nil { return nil, err } genreQuery := `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1` genreRows, _ := m.DB.QueryContext(ctx, genreQuery, movie.ID) genres := make(map[int]string) for genreRows.Next() { var mg MovieGenre err := genreRows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } genreRows.Close() movie.MovieGenre = genres movies = append(movies, &movie) } return movies, nil } func (m *DBModel) GenresAll() ([]*Genre, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `select id, genre_name, created_at, updated_at from genres order by genre_name` rows, err := m.DB.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var genres []*Genre for rows.Next() { var g Genre err := rows.Scan( &g.ID, &g.GenreName, &g.CreatedAt, &g.UpdatedAt, ) if err != nil { return nil, err } genres = append(genres, &g) } return genres, nil } func (m *DBModel) InsertMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `insert movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, movie.Poster, ) if err != nil { return err } return nil } func (m *DBModel) UpdateMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `update movies set title = $1, description = $2, year = $3, release_date = $4, runtime = $5, rating = $6, mpaa_rating = $7, updated_at = $8, poster = $9 where id = $10` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.UpdatedAt, movie.Poster, movie.ID, ) if err != nil { return err } return nil } func (m *DBModel) DeleteMovie(id int) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := "delete from movies where id = $1" _, err := m.DB.ExecContext(ctx, stmt, id) if err != nil { return err } return nil } |
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# docker-compose.yml version: "3.3" services: backend: build: context: . dockerfile: ./Dockerfile-golang ports: - 4000:4000 environment: - TMDB_KEY=$TMDB_KEY volumes: - ./backend-app:/app frontend: build: context: . dockerfile: ./Dockerfile-react volumes: - ./go-movies:/go-movies command: > sh -c "npm run start" ports: - "3000:3000" container_name: frontend postgres: build: context: . dockerfile: ./Dockerfile-postgres environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=secret - POSTGRES_DB=go_movies ports: - "5432:5432" container_name: postgres postgres-gui: image: donnex/pgweb command: -s --bind=0.0.0.0 --listen=8080 --url postgresql://postgres:secret@postgres/go_movies?sslmode=disable links: - postgres:postgres ports: - "9232:8080" depends_on: - postgres |
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
// src/App.tsx import { Dispatch, Fragment, useEffect } from 'react' import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { connect } from 'react-redux' import { setJWTAction } from './redux/actions/setJWTAction' import Home from './components/Home' import Movies from './components/Movies' import Admin from './components/Admin' import Genres from './components/Genres' import OneMovie from './components/OneMovie' import OneGenre from './components/OneGenre' import EditMovie from './components/EditMovie' import Login from './components/Login' import GraphQL from './components/GraphQL' import OneMovieGraphQL from './components/OneMovieGraphQL' const App = (props: any) => { useEffect(() => { const jwtToken = window.localStorage.getItem("jwt") if (jwtToken) { if (props.jwt === "") { props.setJWT(JSON.parse(jwtToken)) } } }) const logout = () => { props.setJWT("") window.localStorage.removeItem("jwt") } let loginLink if (props.jwt === "") { loginLink = <Link to="/login">Login</Link> } else { loginLink = <Link to="/logout" onClick={logout}>Logout</Link> } return ( <Router> <div className="container"> <div className="row"> <div className="col mt-3"> <h1 className="mt-3"> Go Watch a Movie! </h1> </div> <div className="col mt-3 text-end"> {loginLink} </div> <hr className="mb-3" /> </div> <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> <li className="list-group-item"> <Link to="/">Home</Link> </li> <li className="list-group-item"> <Link to="/movies">Movies</Link> </li> <li className="list-group-item"> <Link to="/genres">Genres</Link> </li> { props.jwt !== "" && <Fragment> <li className="list-group-item"> <Link to="/admin/movie/0">Add Movie</Link> </li> <li className="list-group-item"> <Link to="/admin">Manage Catalog</Link> </li> </Fragment> } <li className="list-group-item"> <Link to="/graphql">GraphQL</Link> </li> </ul> </nav> </div> <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/moviesgraphql/:id" component={OneMovieGraphQL} /> <Route path="/movies"> <Movies /> </Route> <Route path="/genre/:id" component={OneGenre} /> <Route exact path="/login" component={(props: any) => <Login {...props} />} /> <Route exact path="/genres"> <Genres /> </Route> <Route exact path="/graphql"> <GraphQL /> </Route> <Route path="/admin/movie/:id" component={EditMovie} /> <Route path="/admin" component={Admin} /> <Route path="/"> <Home /> </Route> </Switch> </div> </div> </div> </Router> ) } // State -> Props const mapStateToProps = (state: {jwt: string}) => ({ jwt: state.jwt }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) // AppコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(App) |
EditMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
// components/EditMovie.tsx import { Dispatch, Fragment, useEffect, useState, SyntheticEvent } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' import { AlertType } from '../models/ui-components' import { Movie } from '../models/movie' import { Link } from 'react-router-dom' import { confirmAlert } from 'react-confirm-alert' import 'react-confirm-alert/src/react-confirm-alert.css' import Input from './form-components/Input' import TextArea from './form-components/TextArea' import Select from './form-components/Select' import Alert from './ui-components/Alert' import './EditMovie.css' import axios from 'axios' const EditMovie = (props: any) => { const [movie, setMovie] = useState<Movie>() const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) // form data const [id, setID] = useState(0) const [title, setTitle] = useState("") const [description, setDescription] = useState("") const [releaseDate, setReleaseDate] = useState("") const [runtime, setRuntime] = useState(0) const [rating, setRating] = useState(0) const [mpaaRating, setMpaaRating] = useState("") const [mpaaOptions, setMpaaOptions] = useState([{}]) useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ( async () => { setMpaaOptions([ {id: "G", value: "G"}, {id: "PG", value: "PG"}, {id: "PG13", value: "PG13"}, {id: "R", value: "R"}, {id: "NC17", value: "NC17"}, ]) // URLからIDを取得 setID(props.match.params.id) if (id > 0) { // Edit await axios.get(`movie/${id}`) .then((response) => { let movie = response.data.movie const releaseDate = new Date(movie.release_date) movie.release_date = releaseDate.toISOString().split("T")[0] setMovie(movie) setTitle(movie.title) setDescription(movie.description) setReleaseDate(movie.release_date) setRuntime(movie.runtime) setRating(movie.rating) setMpaaRating(movie.mpaa_rating) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } else { // New setIsLoaded(true) } } )() }, [id]) const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: Number(runtime), rating: Number(rating), mpaa_rating: mpaaRating, genres: [], poster: "", } // validation let localErrors: string[] = [] if (movie?.title === "") { localErrors.push("title") } if (movie?.release_date === "") { localErrors.push("release_date") } if (movie?.runtime === 0) { localErrors.push("runtime") } if (movie?.rating === 0) { localErrors.push("rating") } setErrors(localErrors) if (errors.length > 0) { return false } const config = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.jwt}` } } await axios.post('admin/editmovie', JSON.stringify(movie), config) .then((response) => { setAlert({ type: 'alert-success', message: 'Changes saved!' }) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } const confirmDelete = (e: SyntheticEvent) => { e.preventDefault() confirmAlert({ title: 'Delete Movie?', message: 'Are you sure?', buttons: [ { label: 'Yes', onClick: async () => { await axios.get(`admin/deletemovie/${id}`) .then((response) => { props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } }, { label: 'No', onClick: () => {} } ] }); } if (error) { // return ( // <div>Error: {error}</div> // ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Add/Edit Movie</h2> <Alert alertType={alert.type} alertMessage={alert.message} /> <hr /> <form method="post" onSubmit={submit}> <Input title={"Title"} className={hasError("title") ? "is-invalid" : ""} type={'text'} name={'title'} value={title} handleChange={setTitle} errorDiv={hasError("title") ? "text-danger" : "d-none"} errorMsg={"Please enter a title"} /> <Input title={"Release Date"} className={hasError("release_date") ? "is-invalid" : ""} type={'date'} name={'release_date'} value={releaseDate} handleChange={setReleaseDate} errorDiv={hasError("release_date") ? "text-danger" : "d-none"} errorMsg={"Please enter a release_date"} /> <Input title={"Runtime"} className={hasError("runtime") ? "is-invalid" : ""} type={'text'} name={'runtime'} value={runtime} handleChange={setRuntime} errorDiv={hasError("runtime") ? "text-danger" : "d-none"} errorMsg={"Please enter a runtime"} /> <Select title={'MPAA Rating'} name={'mpaa_rating'} options={mpaaOptions} value={mpaaRating} handleChange={setMpaaRating} placeholder={'Choose...'} /> <Input title={"Rating"} className={hasError("rating") ? "is-invalid" : ""} type={'text'} name={'rating'} value={rating} handleChange={setRating} errorDiv={hasError("rating") ? "text-danger" : "d-none"} errorMsg={"Please enter a rating"} /> <TextArea title={"Description"} name={'description'} value={description} handleChange={setDescription} rows={3} /> <hr /> <button className="btn btn-primary" type="submit">Save</button> <Link to="/admin" className="btn btn-warning ms-1">Cancel</Link> {id > 0 && ( <a href="#!" onClick={confirmDelete} className="btn btn-danger ms-1"> Delete </a> )} </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(EditMovie) |
GraphQL.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
// components/GraphQL.tsx import { Fragment, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Movie } from '../models/movie' import axios from 'axios' import Input from './form-components/Input' const GraphQL = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [searchTerm, setSearchTerm] = useState("") const performList = () => { const payload = ` { list { id title runtime year description } } ` graphQLRequest(payload, "list") } const performSearch = () => { const payload = ` { search(titleContains: "${searchTerm}") { id title runtime year description } } ` graphQLRequest(payload, "search") } useEffect(() => { ( () => { if (searchTerm === "") { performList() } else { performSearch() } } )() }, [searchTerm]) const graphQLRequest = async (payload: string, type: string) => { const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql', payload, config) .then((response) => { let theList switch (type) { case 'list': theList = response.data.data.list break case 'search': theList = response.data.data.search break } if (theList.length) { setMovies(theList) } else { setMovies([]) } setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>GraphQL</h2> <hr /> <Input title={"Search"} type={"text"} name={"search"} value={searchTerm} handleChange={setSearchTerm} /> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/moviesgraphql/${m.id}`} className="list-group-item list-group-item-action" > <strong>{m.title}</strong> <br /> <small className="text-muted"> ({m.year}) - {m.runtime} minutes </small> <br /> {m.description.slice(0, 100)} </Link> ) })} </div> </Fragment> ) } export default GraphQL |
OneMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
// components/OneMovie.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [], poster: '', }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get(`movie/${props.match.params.id}`) .then((response) => { setMovie(response.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovie |
OneMovieGraphQL.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
// components/OneMovieGraphQL.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovieGraphQL = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [], poster: '', }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { const payload = ` { movie(id: ${props.match.params.id}) { id title runtime year description release_date rating mpaa_rating poster } } ` graphQLRequest(payload) }, []) const graphQLRequest = async (payload: string) => { const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql', payload, config) .then((response) => { setMovie(response.data.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> {movie.poster !== "" && ( <div> <img src={`http://image.tmdb.org/t/p/w200${movie.poster}`} alt="poster" /> </div> )} <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovieGraphQL |
movie.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// models/movie.ts export interface Movie { id: number title: string description: string year: string release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> poster: string } export interface Genre { id: number genre_name: string } |
コメントを残す
コメントを投稿するにはログインしてください。