Skip to content

Commit

Permalink
Merge pull request #45 from canisterism/post-review
Browse files Browse the repository at this point in the history
Post review
  • Loading branch information
canisterism authored Aug 6, 2023
2 parents 2c1de59 + d0f64f3 commit 905702d
Show file tree
Hide file tree
Showing 20 changed files with 998 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit

```bash
$ cd backend
$ rails graphql:schema:dump
$ rails graphql:schema:idl
```
※ /backendに生成されるが、ルートに移動させないとfrontend側で認識されないので注意
### graphqlのコード自動生成
Expand Down
1 change: 1 addition & 0 deletions backend/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ gem 'pg'
gem 'puma', '~> 5.0'
gem 'rack-cors'
gem 'rails', '~> 7.0.4', '>= 7.0.4.3'
# gem 'rspec-rails' #あとで書く!!!
gem 'sass-rails'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

Expand Down
9 changes: 4 additions & 5 deletions backend/app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def execute
context = { current_user: }
result = BackendSchema.execute(query, variables:, context:, operation_name:)
render json: result
# rescue StandardError => e
# Rails.logger.error e.message
# raise e unless Rails.env.development?
rescue StandardError => e
Rails.logger.error e.message
raise e unless Rails.env.development?

# handle_error_in_development(e)
handle_error_in_development(e)
end

private
Expand All @@ -35,7 +35,6 @@ def authenticate_user
# リクエストヘッダーからjwtを取得
token = request.headers['Authorization'].split(' ').last

# TODO: あとでmiddlewareでキャッシュする実装にする
jwks = JwtKeyFetcher.fetch_key

# jwtを検証
Expand Down
24 changes: 24 additions & 0 deletions backend/app/graphql/mutations/delete_review.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Mutations::DeleteReview < Mutations::BaseMutation
null true
argument :review_id, ID, required: true, loads: Types::ReviewType

field :review, Types::ReviewType, null: true
field :errors, [String], null: false

def resolve(review:)
review.destroy!
return { review: review, errors: [] }
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new(e.full_messages, extensions: { "code" => ""})
end

def ready?(**args)
if context[:current_user].present?
super
elsif context[:current_user].profile == args[:review].profile
raise GraphQL::ExecutionError.new('レビューを編集する権限がありません', extensions: { "code" => "UNAUTHORIZED" })
else
raise GraphQL::ExecutionError.new('ログインしてください', extensions: { "code" => "UNAUTHORIZED" })
end
end
end
26 changes: 26 additions & 0 deletions backend/app/graphql/mutations/post_review.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Mutations::PostReview < Mutations::BaseMutation
null true
argument :game_id, ID, required: true, loads: Types::GameType
argument :rating, Float, required: true
argument :body, String, required: false

field :review, Types::ReviewType, null: true
field :errors, [String], null: false

def resolve(game:, rating:, body:)
review = Review.new(game: game, rating:, body:, profile: context[:current_user].profile)
if review.save
{ review: review, errors: [] }
else
{ review: nil, errors: review.errors.full_messages }
end
end

def ready?(**args)
if context[:current_user].present?
super
else
raise GraphQL::ExecutionError.new('ログインしてください', extensions: { "code" => "UNAUTHORIZED" })
end
end
end
26 changes: 26 additions & 0 deletions backend/app/graphql/mutations/update_review.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Mutations::UpdateReview < Mutations::BaseMutation
null true
argument :review_id, ID, required: true, loads: Types::ReviewType
argument :rating, Float, required: true
argument :body, String, required: false

field :review, Types::ReviewType, null: true
field :errors, [String], null: false

def resolve(review:, rating:, body:)
review.update!(rating: rating, body: body)
return { review: review, errors: [] }
rescue ActiveRecord::RecordInvalid => e
raise GraphQL::ExecutionError.new(e.full_messages, extensions: { "code" => "" })
end

def ready?(**args)
if context[:current_user].present?
super
elsif context[:current_user].profile == args[:review].profile
raise GraphQL::ExecutionError.new('レビューを編集する権限がありません', extensions: { "code" => "UNAUTHORIZED" })
else
raise GraphQL::ExecutionError.new('ログインしてください', extensions: { "code" => "UNAUTHORIZED" })
end
end
end
14 changes: 14 additions & 0 deletions backend/app/graphql/types/game_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class GameType < Types::BaseObject
field :rating_average, Float, null: false, description: 'レビューの平均評価'

field :reviews, [Types::ReviewType], null: false
field :my_review, Types::ReviewType, null: true, description: '自分のレビュー'
field :clips, [Types::ClipType], null: false
field :is_clipped, Boolean, null: false, description: 'クリップしたかどうか'
field :platforms, [Types::PlatformType], null: false
field :genres, [Types::GenreType], null: false
field :publisher, Types::PublisherType, null: false
Expand All @@ -29,10 +31,22 @@ def reviews
dataloader.with(Sources::BatchedAssociationsByForeignKey, Review, :game_id).load(object.id)
end

def my_review
dataloader.with(Sources::BatchedAssociationsByForeignKey, Review, :game_id).load(object.id).then do |reviews|
reviews.find { |review| review.profile_id == context[:current_user]&.id }
end
end

def clips
dataloader.with(Sources::BatchedAssociationsByForeignKey, Clip, :game_id).load(object.id)
end

def is_clipped
dataloader.with(Sources::BatchedAssociationsByForeignKey, Clip, :game_id).load(object.id).then do |clips|
clips.any? { |clip| clip.profile_id == context[:current_user]&.id }
end
end

def platforms
dataloader.with(Sources::BatchedAssociationsByManyToMany, Platform, :games_platforms, :game_id,
:platform_id).load(object.id)
Expand Down
9 changes: 3 additions & 6 deletions backend/app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
module Types
class MutationType < Types::BaseObject
# TODO: remove me
field :test_field, String, null: false,
description: 'An example field added by the generator'
def test_field
'Hello World'
end
field :post_review, mutation: Mutations::PostReview
field :update_review, mutation: Mutations::UpdateReview
field :delete_review, mutation: Mutations::DeleteReview
end
end
9 changes: 9 additions & 0 deletions backend/app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ def review(id:)
Review.find(id)
end

field :my_review, Types::ReviewType, null: true do
description 'Find a review by game and profile(from current_user)'
argument :game_id, ID, required: true, loads: Types::GameType
end

def my_review(game:)
Review.find_by(game:, profile_id: context[:current_user]&.id)
end

field :reviews, Types::ReviewType.connection_type, null: false do
description 'Fetch all reviews. filtered by game or profile'
argument :game_id, ID, required: false
Expand Down
1 change: 0 additions & 1 deletion backend/app/graphql/types/review_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
module Types
class ReviewType < Types::BaseObject
implements GraphQL::Types::Relay::Node
field :id, ID, null: false
field :rating, Float, null: false
field :body, String, null: false
field :game, Types::GameType, null: false
Expand Down
2 changes: 1 addition & 1 deletion frontend/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
schema: "../schema.graphql",
documents: "./**/*.ts(x)",
documents: "./**/*.tsx",
generates: {
"./graphql/generated/": {
preset: "client",
Expand Down
63 changes: 63 additions & 0 deletions frontend/components/Review/PostReviewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import ReviewModalPresentation from "@/components/Review/ReviewModalPresentation";
import { PostReviewDocument } from "@/graphql/generated/graphql";
import { useMutation } from "@apollo/client";
import gql from "graphql-tag";

export const postReviewModalMutation = gql`
mutation postReview($input: PostReviewInput!) {
postReview(input: $input) {
review {
id
body
rating
createdAt
profile {
id
displayName
photoUrl
}
}
}
}
`;

export default function PostReviewModal({
gameId,
isOpen,
setIsOpen,
onSubmitComplete,
}: {
gameId: string;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
onSubmitComplete?: () => void;
}) {
const [postReview, { error, data }] = useMutation(PostReviewDocument);

const onSubmit = async (review: { body: string; rating: number }) => {
try {
await postReview({
variables: {
input: {
gameId: gameId,
body: review.body,
rating: review.rating,
},
},
});
onSubmitComplete && onSubmitComplete();
} catch (error) {
console.log({ error });
}
};

return (
<ReviewModalPresentation
body={undefined}
rating={undefined}
isOpen={isOpen}
setIsOpen={setIsOpen}
onSubmit={onSubmit}
/>
);
}
4 changes: 2 additions & 2 deletions frontend/components/Review/ReviewListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ export function ReviewListItem(props: Props) {
</span>
</div>
<div className="gap-1">
<p>{review.body}</p>
<p className="text-gray-300">{review.body}</p>
<span className="text-gray-400 text-sm">
投稿日:{format(new Date(review.createdAt), "yyyy-MM-dd")}
投稿日:{format(new Date(review.createdAt), "yyyy/MM/dd")}
</span>
</div>
</div>
Expand Down
118 changes: 118 additions & 0 deletions frontend/components/Review/ReviewModalPresentation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";

type Props = {
body: string | undefined;
rating: number | undefined;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
onSubmit: ({ body, rating }: { body: string; rating: number }) => void;
};
export default function ReviewModalPresentation({
body = "",
rating = 3,
isOpen,
setIsOpen,
onSubmit,
}: Props) {
const [reviewBody, setReviewBody] = useState(body);
const [reviewRating, setReviewRating] = useState(rating);
const handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setReviewBody(e.target.value);
};
const handleRatingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setReviewRating(Number(e.target.value));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

onSubmit({ body: reviewBody, rating: reviewRating });
setIsOpen(false);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50"
onClose={() => setIsOpen(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-gray-700 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:h-full sm:max-h-max sm:my-8 sm:w-full sm:max-w-screen-sm sm:p-6">
<form onSubmit={handleSubmit}>
<Dialog.Title>
<h3 className="text-lg font-medium leading-6 text-white">
レビューを投稿する
</h3>
</Dialog.Title>
<div className="my-2 flex items-center justify-between gap-5">
<label
htmlFor="review-rating"
className="block text-xl font-medium text-white"
>
{reviewRating}
</label>
<input
id="review-rating"
type="range"
min="1"
max="5"
step="0.5"
className="w-11/12 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-teal-500"
value={reviewRating}
onChange={handleRatingChange}
/>
</div>
<label
htmlFor="review-body"
className="block mb-2 text-sm font-medium text-white"
>
レビュー
</label>
<textarea
id="review-body"
value={reviewBody}
onChange={handleBodyChange}
rows={10}
className="block p-2.5 w-full text-sm rounded-lg border focus:ring-teal-600 focus:border-teal-500 bg-gray-700 border-gray-600 placeholder-gray-400 text-white "
placeholder="ここにあなたのレビューを入力してください。ゲームのグラフィック、ストーリー、キャラクター、音楽など、感じた点を具体的に詳しく書いてみましょう。あなたの意見が他のゲーマーにとって貴重な参考になります。また、ネタバレには注意してください。"
/>

<div className="mt-5 sm:mt-6">
<button
type="submit"
className="inline-flex w-full justify-center rounded-md bg-teal-700 border border-teal-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-teal-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
投稿する
</button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
Loading

0 comments on commit 905702d

Please sign in to comment.