diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f6f8a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# log files +*.log.* +*.log + +# environment informations and credentials +*.env +*.pyc +./frontend/thinks +*.DS_Store diff --git a/README.md b/README.md index 6b828a7..c236ea1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# Mini-Run -aa \ No newline at end of file +# **「暇つぶし革命!遊びながら学び、作って解く新感覚ミニゲーム体験!」** +URL:https://test.croud + +## 実行方法 +``` +docker compose build +docker compose up +``` +URL("localhost:5000")に接続 + +## 使用技術 +|kind |version | +|:--|:--:| +|node.js |node:18 | +|Flask |2.2.5 | +|boto3 |1.26.145 | +|gunicorn |20.1.0 | \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b0511f4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10 + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["flask", "run", "--host=0.0.0.0"] \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..9178668 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,26 @@ +from flask import Flask +from flask_cors import CORS +import router +from config import config + +def create_app(): + app = Flask(__name__) + + app.config.from_object(config.Config) + + app.register_blueprint(router.router) + + app.config['JSON_AS_ASCII'] = False #日本語文字化け対策 + app.config["JSON_SORT_KEYS"] = False #ソートをそのまま + CORS( + app, + resources = { + r"/api/*": {"origins": ["http://localhost", "http://localhost:3000","http://localhost:8032"]} + } + ) + + return app + +if __name__ == "__main__": + app = create_app() + app.run(host='0.0.0.0', debug=True, port=5001, threaded=True, use_reloader=False) \ No newline at end of file diff --git a/backend/config/config.py b/backend/config/config.py new file mode 100644 index 0000000..dc64d31 --- /dev/null +++ b/backend/config/config.py @@ -0,0 +1,7 @@ +import os +from dataclasses import dataclass + +@dataclass +class Config: + DEBUG = os.getenv("DEBUG") + DATABASE_URI= "" diff --git a/backend/config/logging.json b/backend/config/logging.json new file mode 100644 index 0000000..2e66282 --- /dev/null +++ b/backend/config/logging.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "formatters": { + "default": { + "format": "[%(asctime)s] [%(levelname)s] : %(message)s" + } + }, + "loggers": { + "file": { + "handlers": ["file"], + "level": "WARN", + "qualname": "file", + "propagate": "no" + }, + "wsgi": { + "handlers": ["wsgi"], + "level": "WARN", + "qualname": "wsgi", + "propagate": "no" + } + }, + "handlers": { + "file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "formatter": "default", + "filename": "./logs/application.log" + }, + "wsgi": { + "class": "logging.StreamHandler", + "stream": "ext://flask.logging.wsgi_errors_stream", + "formatter": "default" + } + }, + "root": { + "level": "WARN", + "handlers": ["file", "wsgi"] + } +} diff --git a/backend/controller/game_controller.py b/backend/controller/game_controller.py new file mode 100644 index 0000000..454f980 --- /dev/null +++ b/backend/controller/game_controller.py @@ -0,0 +1,257 @@ +from flask import make_response, jsonify, request +from engine.dynamo_client import DynamoClient +import hashlib +from engine.s3_client import S3Client +#from werkzeug.utils import secure_filename + + +class GameController: + def __init__(self): + self.table_name = "minirun_game_table" + self.db_client = DynamoClient(table_name="minirun_game_table") + self.s3_client = S3Client() # S3専用クラスを利用 + + def get_game_random(self): + response = self.db_client.get_random() + return make_response(jsonify({ + 'code': 200, + 'game': response + })) + + def get_last_id(self) -> int: + response = self.db_client.scan(TableName=self.table_name, ProjectionExpression="id") + items = response.get('Items', []) + if not items: + return 0 + ids = [int(item['id']['N']) for item in items] + return max(ids) + + def create_quiz(self): + # リクエストデータを取得 + data = request.get_json() + question = data['question'] + selects = data['selects'] + answer_idx = data['answer_idx'] - 1 + data['answer_idx'] = answer_idx - 1 + last_id = self.get_last_id() + data['id'] = last_id + 1 + + # バリデーションを実装 + if not isinstance(question, str) or not question.strip(): + return make_response(jsonify({'code': 400, 'message': 'Invalid question'}), 400) + if not isinstance(selects, list) or len(selects) != 4: + return make_response(jsonify({'code': 400, 'message': 'Invalid selects'}), 400) + if not isinstance(answer_idx, int) or not (0 <= answer_idx <= 3): + return make_response(jsonify({'code': 400, 'message': 'Invalid answer_idx'}), 400) + + # DBに保存 + try: + if self.db_client.create_quiz(data): + return make_response(jsonify({'code': 200, 'message': 'Quiz created successfully'}), 200) + else: + return make_response(jsonify({'code': 500, 'message': 'Failed to create quiz'}), 500) + except Exception as e: + # エラーハンドリングを実装 + return make_response(jsonify({'code': 500, 'message': str(e)}), 500) + + def create_intro(self): + data = request.get_json() + music_title_answer = data['music_title_answer'] + #music_data = data['music_data'] # musicのデータ本体 + last_id = self.get_last_id() + data['id'] = last_id + 1 + + if 'file' not in request.files: + return jsonify({"error": "No file part"}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({"error": "No selected file"}), 400 + # ファイル名を安全にする + #filename = secure_filename(file.filename) + filename = file.filename + # ファイル名のハッシュを生成 + file_hash = hashlib.sha256(filename.encode('utf-8')).hexdigest() + + try: + # S3にファイルをアップロード + upload_to_s3(file, file_hash, file.content_type) + # DynamoDBにメタデータを保存 + self.db_client.save_file_metadata(data, file_hash, filename) + return jsonify({"message": "File uploaded successfully", "file_hash": file_hash}), 200 + except Exception as e: + return jsonify({"error": str(e)}), 500 + + + # バリデーションを実装 + if not isinstance(music_title_answer, str) or not music_title_answer.strip(): + return make_response(jsonify({'code': 400, 'message': 'Invalid music_title_answer'}), 400) + if not music_data: + return make_response(jsonify({'code': 400, 'message': 'Invalid music_data'}), 400) + + # DBに保存 + try: + if self.db_client.create_intro(data): + return make_response(jsonify({'code': 200, 'message': 'Intro created successfully'}), 200) + else: + return make_response(jsonify({'code': 500, 'message': 'Failed to create intro'}), 500) + except Exception as e: + return make_response(jsonify({'code': 500, 'message': str(e)}), 500) + + def create_search_diff(self): + try: + # フォームデータとファイルを取得 + images = request.files.getlist('images') # 画像ファイルのリスト + answer_idx = request.form.get('answer_idx') # 正解の番号(request.filesを操作した後にrequest.get_json()を使用するとエラーが発生する可能性があるらしい) + + # バリデーションチェック + if not images or len(images) != 9: + return make_response(jsonify({'code': 400, 'message': 'Invalid images list'}), 400) + if answer_idx is None or not answer_idx.isdigit() or not (0 <= int(answer_idx) - 1 < 9): + return make_response(jsonify({'code': 400, 'message': 'Invalid correct index'}), 400) + + answer_idx = int(answer_idx) - 1 + + # 画像ファイルをS3に保存 + image_hashes = [] + for image in images: + #filename = secure_filename(image.filename) + filename = image.filename + file_hash = hashlib.sha256(filename.encode('utf-8')).hexdigest() + # S3にアップロード + self.s3_client.upload_fileobj(image, file_hash) + image_hashes.append(file_hash) + + # DynamoDBに保存するデータを作成 + data_to_save = { + 'id': self.get_last_id() + 1, # 新しいID + 'genre': 'search_diff', # ジャンルを指定 + 'image_hashes': image_hashes, # 画像ハッシュリスト + 'answer_idx': answer_idx # 正解のインデックス + } + + # データベースに保存 + if self.db_client.create_search_diff(data_to_save): + return make_response(jsonify({'code': 200, 'message': 'Search diff created successfully'}), 200) + else: + return make_response(jsonify({'code': 500, 'message': 'Failed to create search diff'}), 500) + except Exception as e: + return make_response(jsonify({'code': 500, 'message': str(e)}), 500) + + + def play_quiz(self): + try: + # データベースからランダムに問題を取得 + #response = self.db_client.get_random() + response = self.db_client.get_random_2(genre="quiz") + if response: + quiz_data = { + "question": response.get('question', {}).get('S', ''), + "selects": [select.get('S', '') for select in response.get('selects', {}).get('L', [])], + "answer_idx": int(response.get('answer_idx', {}).get('N', -1)) + } + return make_response(jsonify({ + 'code': 200, + 'game_content': quiz_data + }), 200) + else: + return make_response(jsonify({ + 'code': 404, + 'message': 'No quiz found' + }), 404) + except Exception as e: + return make_response(jsonify({ + 'code': 500, + 'message': str(e) + }), 500) + + def play_intro(self): + try: + # データベースからランダムに問題を取得 + #response = self.db_client.get_random() + response = self.db_client.get_random_2(genre="introdon") + if response: + intro_data = { + "music_title_answer": response.get('music_title_answer', {}).get('S', ''), + #"music_data": response.get('music_data', {}).get('B', '') + } + return make_response(jsonify({ + 'code': 200, + 'game_content': intro_data + }), 200) + else: + return make_response(jsonify({ + 'code': 404, + 'message': 'No intro found' + }), 404) + except Exception as e: + return make_response(jsonify({ + 'code': 500, + 'message': str(e) + }), 500) + + def play_search_diff(self): + try: + # データベースからランダムに問題を取得 + response = self.db_client.get_random_2(genre="search_diff") + if response: + # ハッシュ値リストを取得 + image_hashes = response.get("image_hashes", []) + answer_idx = response.get("answer_idx", -1) + # ハッシュ値を元にS3から画像データを取得 + image_data_list = [] + for image_hash in image_hashes: + try: + image_data = self.s3_client.get_s3_file_data(image_hash) + image_data_list.append(image_data) + except Exception as s3_error: + print(f"Error getting image data from S3: {e}") + return make_response(jsonify({ + 'code': 500, + 'message': f"Failed to retrieve image for hash: {image_hash}" + }), 500) + + search_diff_data = { + "images": image_data_list, # 画像データリスト + "answer_idx": answer_idx + } + return make_response(jsonify({ + 'code': 200, + 'game_content': search_diff_data + }), 200) + else: + return make_response(jsonify({ + 'code': 404, + 'message': 'No search_diff found' + }), 404) + except Exception as e: + return make_response(jsonify({ + 'code': 500, + 'message': str(e) + }), 500) + + + def play_diffshot(self): + try: + # データベースからランダムに問題を取得 + response = self.db_client.get_random() + if response: + diffshot_data = { + "picture_data": response.get('picture_data', {}).get('S', ''), + "picture_data_answer": response.get('picture_data_answer', {}).get('S', ''), + "answer_points": response.get('answer_points', {}).get('L', []) + } + return make_response(jsonify({ + 'code': 200, + 'game_content': diffshot_data + }), 200) + else: + return make_response(jsonify({ + 'code': 404, + 'message': 'No diffshot found' + }), 404) + except Exception as e: + return make_response(jsonify({ + 'code': 500, + 'message': str(e) + }), 500) \ No newline at end of file diff --git a/backend/engine/dynamo_client.py b/backend/engine/dynamo_client.py new file mode 100644 index 0000000..04a2a09 --- /dev/null +++ b/backend/engine/dynamo_client.py @@ -0,0 +1,161 @@ +import os +import boto3 +import random + +class DynamoClient: + def __init__(self, table_name: str) -> None: + self.db = boto3.client('dynamodb') + self.table_name = table_name + #self.db = boto3.resource('dynamodb', region_name=REGION_NAME) + #self.table_name = table_name or DYNAMODB_TABLE + #self.table = self.db.Table(self.table_name) + + def get_all(self) -> list: + '''対象のテーブルからレコード全件を取得するメソッド + ''' + response = self.db.scan(TableName=self.table_name) + items = response.get('Items', []) + return items + + def get_random(self) -> None: + '''対象のテーブルからランダムに1件レコードを取得するメソッド + ''' + try: + response_all = self.db.scan(TableName=self.table_name) + items = response_all.get('Items', []) + if not items: + return None + random_item = random.choice(items) + return random_item + except Exception as e: + print(f"Error retrieving random item: {e}") + return None + + def get_random_2(self, genre: str) -> None: + '''指定されたジャンルの中からランダムに1件レコードを取得するメソッド''' + # 指定されたジャンルのアイテムをスキャン + response_all = self.db.scan( + TableName=self.table_name, + FilterExpression="genre = :genre", + ExpressionAttributeValues={":genre": {"S": genre}} + ) + items = response_all.get('Items', []) + if not items: + return None + if genre == "quiz": + # 有効なキーを取得 + valid_keys = [item['id']['N'] for item in response_all['Items']] + random_key = random.choice(valid_keys) + response = self.db.get_item(TableName=self.table_name, Key={"id": {"N": str(random_key)}}) + return response.get('Item') + if genre == "introdon": + # 有効なキーを取得 + valid_keys = [item['id']['N'] for item in response_all['Items']] + random_key = random.choice(valid_keys) + selected_item = random.choice(items) + + # 音声データのハッシュ値と元の名前を取得 + file_hash = selected_item['file_hash']['S'] + original_name = selected_item['original_name']['S'] + + # 必要に応じて他の処理を追加可能 + return { + "file_hash": file_hash, + "original_name": original_name, + } + if genre == "search_diff": + selected_item = random.choice(items) + # 画像データのハッシュリストと正解インデックスを取得 + image_hashes = [hash_item["S"] for hash_item in selected_item['image_hashes']['L']] + answer_idx = int(selected_item['answer_idx']['N']) + + # 必要なデータを返す + return { + "image_hashes": image_hashes, + "answer_idx": answer_idx + } + else: + return None + + + def scan(self, **kwargs): + return self.db.scan(**kwargs) + + def create_quiz(self, data: dict) -> None: + '''対象のテーブルにクイズデータを登録するメソッド + ''' + + try: + response = self.db.put_item(TableName=self.table_name, Item={ + 'id': {'N': str(data['id'])}, + 'question': {'S': data['question']}, + 'selects': {'L': [{'S': s} for s in data['selects']]}, + 'answer_idx': {'N': str(data['answer_idx'])}, + 'likes': {'N': '0'}, + 'genre': {'S': 'quiz'} + }) + return True if response['ResponseMetadata']['HTTPStatusCode'] == 200 else False + except Exception as e: + print(f"Error saving data to DynamoDB: {e}") + return False + + + def create_intro(self, data: dict) -> None: + '''対象のテーブルにイントロデータを登録するメソッド + ''' + client = boto3.client('dynamodb') + response = client.put_item(TableName=self.table_name, Item={ + 'id': {'N': str(data['id'])}, + 'music_title_answer': {'S': data['music_title_answer']}, + 'music_data': {'B': data['music_data']}, + 'likes': {'N': '0'} + }) + + def create_search_diff(self, data: dict) -> bool: + """ + DynamoDBに間違い画像選択ゲームのデータを保存する + """ + try: + item = { + 'id': {'N': str(data['id'])}, + 'genre': {'S': data['genre']}, + 'image_hashes': {'L': [{'S': hash} for hash in data['image_hashes']]}, + 'answer_idx': {'N': str(data['answer_idx'])}, + 'likes': {'N': '0'} # 初期値として0を設定 + } + response = self.db.put_item( + TableName=self.table_name, + #Item=data + Item=item + ) + # ステータスコードが200であれば成功 + return response['ResponseMetadata']['HTTPStatusCode'] == 200 + except Exception as e: + print(f"Error saving data to DynamoDB: {e}") + return False + + def save_file_metadata(self, data: dict, file_hash: str, original_name: str) -> bool: + """ + DynamoDBにファイルのメタデータを保存する関数 + + Args: + file_hash (str): ファイルのハッシュ値 + original_name (str): 元のファイル名 + + Returns: + bool: 成功した場合はTrue、失敗した場合はFalse + """ + try: + client = boto3.client('dynamodb') + # DynamoDBにデータを登録 + response = client.put_item(Item={ + 'id': {'N': str(data['id'])}, + 'file_hash': file_hash, + 'original_name': original_name + }) + return response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 200 + except Exception as e: + print(f"Error saving file metadata to DynamoDB: {e}") + return False + + diff --git a/backend/engine/s3_client.py b/backend/engine/s3_client.py new file mode 100644 index 0000000..5ea2e81 --- /dev/null +++ b/backend/engine/s3_client.py @@ -0,0 +1,117 @@ +import boto3 +import os +from dotenv import load_dotenv + +# .envファイルを読み込む +load_dotenv() + + +class S3Client: + def __init__(self): + """ + S3クライアントを初期化し、バケット名を設定 + """ + self.s3_client = boto3.client( + "s3", + #aws_access_key_id=os.getenv("S3_ACCESS_KEY"), + #aws_secret_access_key=os.getenv("S3_SECRET_KEY"), + #endpoint_url=os.getenv("S3_ENDPOINT") # 必要に応じて指定(MinIOやローカルS3の場合) + ) + self.bucket_name = os.getenv("S3_BUCKET") # 環境変数からバケット名を取得 + + def upload_fileobj(self, file, file_key): + """ + S3にファイルをアップロードするメソッド + + Args: + file: アップロードするファイルオブジェクト + file_key: S3に保存するオブジェクトのキー + + Raises: + Exception: アップロード失敗時に例外を発生 + """ + try: + self.s3_client.upload_fileobj( + file, + self.bucket_name, # バケット名は内部で指定 + file_key, + ExtraArgs={'ContentType': file.content_type} # MIMEタイプを指定 + ) + print(f"Uploaded {file_key} to {self.bucket_name}") + except Exception as e: + print(f"Failed to upload file: {e}") + raise e + +## boto3クライアントの初期化 +#s3_client = boto3.client( +# "s3", +# #aws_access_key_id=os.getenv("S3_ACCESS_KEY"), +# #aws_secret_access_key=os.getenv("S3_SECRET_KEY"), +# #endpoint_url=os.getenv("S3_ENDPOINT") # ローカル時のみ +#) +# +#def upload_to_s3(file, file_hash, content_type): +# """ +# S3にファイルをアップロードする関数 +# +# Args: +# file: アップロードするファイルオブジェクト +# file_hash: S3に保存するオブジェクトのキー(ユニークな名前) +# content_type: ファイルのContent-Type +# +# Raises: +# Exception: アップロードが失敗した場合 +# """ +# try: +# # 環境変数からS3バケットを取得 +# s3_bucket = os.getenv("S3_BUCKET") +# +# # S3にファイルをアップロード +# s3_client.upload_fileobj( +# file, +# s3_bucket, +# file_hash, # ファイル名の代わりにハッシュを使用 +# ExtraArgs={"ContentType": content_type} +# ) +# print(f"File uploaded successfully to S3 with key: {file_hash}") +# except Exception as e: +# print(f"Error uploading file to S3: {e}") +# raise e +# +#def get_s3_file_url(file_key: str) -> str: +# """ +# S3のファイルにアクセスするための署名付きURLを生成する関数 +# +# Args: +# file_key (str): S3に格納されているオブジェクトのキー +# +# Returns: +# str: 署名付きURL +# """ +# try: +# url = s3_client.generate_presigned_url( +# 'get_object', +# Params={'Bucket': os.getenv("S3_BUCKET"), 'Key': file_key}, +# ExpiresIn=3600 # URLの有効期限(秒単位) +# ) +# return url +# except Exception as e: +# print(f"Error generating S3 presigned URL: {e}") +# return "" +# +#def upload_fileobj(self, file, bucket_name, file_hash): +# """ +# S3にファイルをアップロードする +# """ +# try: +# self.s3_client.upload_fileobj( +# file, +# bucket_name, +# file_hash, +# ExtraArgs={'ContentType': file.content_type} +# ) +# print(f"Uploaded {file_hash} to {bucket_name}") +# except Exception as e: +# print(f"Failed to upload file: {e}") +# raise e +# \ No newline at end of file diff --git a/backend/initialize_db.py b/backend/initialize_db.py new file mode 100644 index 0000000..a58d0f4 --- /dev/null +++ b/backend/initialize_db.py @@ -0,0 +1,21 @@ +import os +import boto3 + +dynamodb = boto3.client('dynamodb', endpoint_url=os.getenv("DYNAMO_ENDPOINT")) + +table_name = "game_table" + +def main(): + dynamodb.create_table( + TableName=table_name, + KeySchema=[ + {"AttributeName": "id", "KeyType": "HASH",}, + ], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "N"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/logger.py b/backend/logger.py new file mode 100644 index 0000000..a948aa5 --- /dev/null +++ b/backend/logger.py @@ -0,0 +1,14 @@ +from flask import request, current_app +from functools import wraps + +def http_request_logging(f): + @wraps(f) + def decorated_function(*args, **kwargs): + logger = current_app.logger + try: + logger.info('%s - %s - %s - %s', request.remote_addr, request.method, request.url, request.query_string) + except Exception as e: + logger.exception(e) + pass + return f(*args, **kwargs) + return decorated_function diff --git a/backend/model/games.py b/backend/model/games.py new file mode 100644 index 0000000..892304d --- /dev/null +++ b/backend/model/games.py @@ -0,0 +1,17 @@ +class Quiz: + def __init__(self, id: int, question: str, selects: list, answer_idx: int, likes: int=0) -> None: + self.id = id + self.question = question + self.selects = selects + self.answer_idx = answer_idx + self.likes = likes + + # TODO: __repr__(self)をつくる + +class IntroDon: + def __init__(self, game_id: int, question: str, selects: list, answer: str, likes: int=0) -> None: + self.game_id = game_id + self.question = question + self.selects = selects + self.answer = answer + self.likes = likes \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f4589fa --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +Flask==2.2.5 +boto3==1.26.145 +gunicorn==20.1.0 +flask_cors +python-dotenv \ No newline at end of file diff --git a/backend/router.py b/backend/router.py new file mode 100644 index 0000000..4b696ee --- /dev/null +++ b/backend/router.py @@ -0,0 +1,72 @@ +from flask import Blueprint +from controller.game_controller import GameController +from logging import config +from json import load +import logger +import boto3 + +## クライアントの初期化 +#s3_client = boto3.client( +# "s3", +# aws_access_key_id=S3_ACCESS_KEY, +# aws_secret_access_key=S3_SECRET_KEY, +# endpoint_url=S3_ENDPOINT, # MinIOの場合 +#) +#dynamodb = boto3.resource("dynamodb", region_name=REGION_NAME) +#dynamodb_table = dynamodb.Table(DYNAMODB_TABLE) + +# Generate Router Instance +router = Blueprint('router', __name__) +controller = GameController() + +# Read Logging Configuration +with open("./config/logging.json", "r", encoding="utf-8") as f: + config.dictConfig(load(f)) + +@router.route("/", methods=['GET']) +@logger.http_request_logging +def hello_world(): + return "Hello World!!" + +@router.route("/api/play/random", methods=['GET']) +@logger.http_request_logging +def api_play_random_quiz(): + return controller.get_game_random() + +@router.route("/api/create/quiz", methods=['POST']) +@logger.http_request_logging +def api_create_quiz(): + return controller.create_quiz() + +@router.route("/api/create/intro", methods=['POST']) +@logger.http_request_logging +def api_create_intro(): + return controller.create_intro() + +@router.route("/api/create/searchdiff", methods=['POST']) +@logger.http_request_logging +def api_create_search_diff(): + return controller.create_search_diff() + +@router.route("/api/play/quiz", methods=['GET']) +@logger.http_request_logging +def api_play_quiz(): + return controller.play_quiz() + +@router.route("/api/play/intro", methods=['GET']) +@logger.http_request_logging +def api_play_intro(): + return controller.play_intro() + +@router.route("/api/play/searchdiff", methods=['GET']) +@logger.http_request_logging +def api_play_search_diff(): + return controller.play_search_diff() + + +@router.after_request +def after_request(response): + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization') + response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS') + return response diff --git a/backend/service/game_service.py b/backend/service/game_service.py new file mode 100644 index 0000000..941892c --- /dev/null +++ b/backend/service/game_service.py @@ -0,0 +1,10 @@ +from flask import make_response, jsonify + + +def get_game_logic(): + game = Game.get_game_list() + game_schema = GameSchema(many=True) + return make_response(jsonify({ + 'code': 200, + 'game': game + })) diff --git a/backend/settings.py b/backend/settings.py new file mode 100644 index 0000000..e69de29 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..06e1dee --- /dev/null +++ b/compose.yml @@ -0,0 +1,60 @@ +services: + frontend: + build: ./frontend + container_name: frontend + ports: + - "8080:80" + volumes: + - ./frontend:/app + depends_on: + - backend + + backend: + build: ./backend + container_name: backend + ports: + - "5001:5000" + volumes: + - ./backend:/app + env_file: + - backend/.env + depends_on: + - dynamodb + - minio + + dynamodb: + image: amazon/dynamodb-local + container_name: dynamodb + user: root + command: -jar DynamoDBLocal.jar -sharedDb -dbPath /data + ports: + - "8000:8000" + volumes: + - dynamodb_data:/data + + dynamodb-admin: + image: aaronshaf/dynamodb-admin:latest + container_name: dynamodb-admin + environment: + - DYNAMO_ENDPOINT=http://dynamodb:8000 + ports: + - 8001:8001 + depends_on: + - dynamodb + + minio: + image: minio/minio + container_name: minio + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=miniominio + - MINIO_ROOT_PASSWORD=miniominio + command: server --console-address ":9001" /data + volumes: + - minio_data:/data + +volumes: + dynamodb_data: + minio_data: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..6e8acef --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:alpine +COPY . /usr/share/nginx/html diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..275d795 --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,26 @@ +from flask import Flask, render_template +from flask_cors import CORS + + +app = Flask(__name__) +# CORS(app,resources={r"/*":{"origins":"http://127.0.0.1:8030/"}}) + +CORS(app, resources={r"/*": { + "origins": ["http://127.0.0.1:8030", "https://example.com"], + "methods": ["GET", "POST"], + "allow_headers": [r"*"] +}}) + + +def viewPointMake(theme:str ,content:str): + # app.make + return (theme,content) + + + +@app.route("/") +def site_start(): + return render_template("index.html") + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8120, debug=False) \ No newline at end of file diff --git a/frontend/game.js b/frontend/game.js new file mode 100644 index 0000000..71c9f11 --- /dev/null +++ b/frontend/game.js @@ -0,0 +1,710 @@ +const homeScreen = document.getElementById("home"); +const quiz_Screen = document.getElementById("quiz-container"); +const differentPoint_Screen = document.getElementById("differentPoint-container"); +const introdon_Screen = document.getElementById("introDon-container"); + +const playScreen = document.getElementById("play-mode"); +const createScreen = document.getElementById("create-mode"); + +const resultScreen = document.getElementById("result"); +const gameModeScreen = document.getElementById("game-mode"); +const selectGameScreen = document.getElementById("SelctGameKind"); +const createQuizScreen = document.getElementById("create_quizScreen"); + + + +document.addEventListener("DOMContentLoaded", () => { + const startButton = document.getElementById("start-button"); + + const createButton = document.getElementById("create-button"); + const playButton = document.getElementById("play-button"); + const nextButton = document.querySelector(".next-button"); + const quitButton = document.querySelector(".quit-button"); + + + const gameKindOptions = document.querySelectorAll(".gamekind-option"); + + //quiz + const quiz_select_options = document.querySelectorAll(".quiz_selects"); + const quiz_qustion = document.getElementById("question_text"); + const quiz_result = document.getElementById("quiz-judge"); + + const statusText = document.querySelector(".status"); + const answerText = document.querySelector(".answer"); + + + + + // ゲームデータ + let gameMode = "play"; + + createButton.addEventListener("click",()=>{ + gameMode = "create"; + }); + playButton.addEventListener("click",()=>{ + gameMode = "play"; + }); + + //ゲーム選択画面でそのままLoadingKeyに入れるようにするための対策 + const gamekind = { + "クイズ":"quiz", + "間違い探し":"differentPoint", + "イントロドン":"introdon" + } + + //bug + const gameSetting = { + "create":{ + "クイズ":"quiz", + "間違い探し":"differentPoint", + "イントロドン":"introdon" + }, + "play":{ + "クイズ":"create_quiz", + "間違い探し":"create_differentPoint", + "イントロドン":"create_introdon" + } + } + + + + function loadQuizQuestion(url, callback) { + const getUrl = url + "/api/play/quiz"; + + fetch(getUrl, { + method: "GET", + }) + .then(response => { + if (!response.ok) { + throw new Error(`ステータスエラー: ${response.status}`); + } + return response.json(); // JSONを解析 + }) + .then(data => { + callback(null, data); // データをコールバックで返す + }) + .catch(error => { + callback(error, null); // エラーをコールバックで返す + }); + } + + + + /** + * 質問を書き込む + * @param question_text { string } 質問文 + * @param selects (list-string)選択肢で表示するもの + * @param answer_number (int)selectsでの連番-何番目の選択肢を正解にするか、 + * その名前のdisplayを表示する + */ + + function set_quiz(question_text,selects,answer_number) + { + quiz_qustion.textContent = question_text; + const quiz_contents = quiz_select_options.item(0).children; + for(var i = 0; i < 4;i++) + { + quiz_contents[i].textContent = selects[i]; + } + correctAnswer = selects[answer_number]; + } + + + function rd(max){ + return Math.floor(Math.random() * max) + } + console.log(rd(2)); + /** + * That option discplayis + * @param display (string)この引数にhome,quiz,result,game_select,game_modeなどをstringで入れると、 + * その名前のdisplayを表示する + */ + function displayLoadRequest(display) + { + + createScreen.classList.add("d-none"); + playScreen.classList.add("d-none"); + if(gameMode == "create"){ + createScreen.classList.remove("d-none"); + }else if (gameMode == "play"){ + playScreen.classList.remove("d-none"); + } + + gameModeScreen.classList.add("d-none"); + selectGameScreen.classList.add("d-none"); + + quiz_Screen.classList.add("d-none"); + differentPoint_Screen.classList.add("d-none"); + introdon_Screen.classList.add("d-none"); + + resultScreen.classList.add("d-none"); + homeScreen.classList.add("d-none"); + create_quizScreen.classList.add("d-none"); + + + console.log(`display:${display}`); + + if (display == "home") + { + homeScreen.classList.remove("d-none"); + } + + //game-play + else if (display == "quiz") + { + + loadQuizQuestion("http://localhost:5001", (error, data) => { + if (error) { + console.error("エラー:", error); + } else { + console.log("取得したクイズデータ:", data); + + set_quiz(data["game_content"]["question"],data["game_content"]["selects"],data["game_content"]["answer_idx"]); + } + }); + quiz_Screen.classList.remove("d-none"); + + + } + else if (display == "differentPoint") + { + differentPoint_Screen.classList.remove("d-none"); + } + else if (display == "introdon") + { + introdon_Screen.classList.remove("d-none"); + } + + //gameCreate + else if (display == "create_quiz") + { + + createQuizScreen.classList.remove("d-none"); + + + } + + else if (display == "create_differentPoint") + { + + } + + else if (display == "create_introdon") + { + + } + + + + + + + else if (display == "game_select") + { + selectGameScreen.classList.remove("d-none"); + } + + else if (display == "game_mode") + { + gameModeScreen.classList.remove("d-none"); + } + + else if (display == "result") + { + resultScreen.classList.remove("d-none"); + } + } + + + + displayLoadRequest("home"); + + + startButton.addEventListener("click", () => { + displayLoadRequest("game_mode"); + }); + + playButton.addEventListener("click",()=>{ + console.log("呼ばれてる1") + displayLoadRequest("game_select"); + }); + + createButton.addEventListener('click',()=>{ + console.log("呼ばれてる2") + displayLoadRequest("create_quiz"); + }); + + //gameMode + + + // ============================================================クイズ================================================================================ +//============================================================================================================================================ + + quiz_select_options.forEach(option => { + console.log(option[0]); + option.addEventListener("click", (event) => { + const selectedAnswer = event.target.textContent; + + // 判定処理 + if (selectedAnswer === correctAnswer) + { + console.log("正解"); + quiz_result.textContent = "正解!" + quiz_result.style.color = "#28a745"; + } + + + else + { + console.log("不正解"); + quiz_result.textContent = "残念、不正解…" + quiz_result.style.color = "#dc3545"; + } + + + // answerText.textContent = correctAnswer; // 正解を表示 + + // 結果画面へ移動 + displayLoadRequest("result"); + }); + }); + +//============================================================================================================================================ +//============================================================================================================================================ +//========================================間違い探し================================================================================ + + const images_list = document.querySelectorAll('.different-image'); + const debug_log = { + "postion":false, + "isDevicePhone":false + } + + //アクセスしているのがスマホかPCかを確認する + function isSmartPhone() + { + if (navigator.userAgent.match(/iPhone|Android.+Mobile/)) + { + console.log(`UserAgent${navigator.userAgent}, phone:True`); + return true; + } + + else + { + console.log(`UserAgent${navigator.userAgent},phone:False`); + return false; + } + } + + + const isUsePhone = isSmartPhone(); + + //request-date + + /** + * @param ImageList srcに代入するためのURL-S3から引っ張ってくるためのキーを指定する + * @param answer_map クリックするとどこが間違ってるかを教えてくれる + * @param CorrectAnswer answer_mapの連番でどれが正解したかを保存する + */ + + let questionDate = { + "ImageList":[], + "answer_map":[[116,69],[42,51]], + "CorrectAnswer":[false,false], + } + + + const imgs = document.querySelectorAll(".imgs"); + + imgs.forEach(img =>{ + questionDate["ImageList"].push([img.src,img.naturalWidth,img.naturalHeight]); + }) + console.log(questionDate["ImageList"]); + LoadImages(0,questionDate["ImageList"][0]); + LoadImages(1,questionDate["ImageList"][1]); + + + + let userDate = { + "lastTouchPos":[25.32, 3452.3], + "selectTouchPostions":[[2432,343],[2432,343]] + } + + + let userClick_history = []; + let canvas_history = [[],[]] + const canvas_images = document.querySelectorAll(".different-image"); + + /** + * 画像をロードする関数です。 + * @param images_number (int)連番の値 + * @param images_number ()連番の値 + */ + function LoadImages(images_number,LoadImage) + { + // images_list[images_number].HTMLimages = LoadImage + var drowCTX = images_list[images_number].getContext('2d'); + var d_image = new Image(); + d_image.src = LoadImage[0]; + + images_list[images_number].width = LoadImage[1]; // 元画像の幅 + images_list[images_number].height = LoadImage[2]; // 元画像の高さ + console.log(`image:${LoadImage[0]}, dx:0, dy:0, Dwidth:${LoadImage[1]}, Dheight:${LoadImage[2]}`); + drowCTX.drawImage(d_image, 0, 0, LoadImage[1], LoadImage[2]); + + } + + + console.log(images_list[0]); + + /** + * 指定した場所を描画する処理 + * @param map 一次元の配列です{34,42}のような形式で送ってください + */ + function Drow_ScuessImagePostion(map){ + images_list.forEach(img =>{ + var drowCtx = img.getContext("2d"); + drowCtx.beginPath(); + drowCtx.arc(map[0], map[1], 2, 0, Math.PI * 2); + + drowCtx.fillStyle = 'red'; + if(Check_TouchImagePostion_CorrectAnswer([map[0],map[1]],questionDate["answer_map"],10)) + { + drowCtx.fillStyle = 'green'; + } + + drowCtx.fill(); + drowCtx.stroke(); + }) + } + + + + + + //画像のどこをタップしたかを調べる処理 + images_list.forEach(image => + { + image.addEventListener("click",(c)=> + { + console.log(c.offsetX,c.offsetY); + + + userDate["lastTouchPos"][0] = c.offsetX; + userDate["lastTouchPos"][1] = c.offsetY; + + Drow_ScuessImagePostion([c.offsetX,c.offsetY]); + if (debug_log["postion"] == true){ + console.log( + Check_TouchImagePostion_CorrectAnswer( + userDate["lastTouchPos"], + questionDate["answer_map"], + 10 + ) + + ); + } + + }); + }); + + + /** + * 正解の場所かどうかを判定する処理 + * @param touchPos 要素数2の1次元の配列 + * @param answer_map 間違い場所を2次元配列で管理しているもの + * @param range どれくらいのpx誤差を許容するか? + */ + function Check_TouchImagePostion_CorrectAnswer(touchPos,answer_map,range){ + let isTouchAnswer = false; + answer_map.forEach(ans_map => { + // isTouchAnswer = false; + console.log(`helloWOrld${ans_map}`) + console.log(`Check_touchPosCorrectAnswer\n SelectPos-x:${touchPos[0]},y:${touchPos[1]} \n AnswerPos-x:${ans_map[0]}, y:${ans_map[1]} \n range:${range}`); + + + //0 = x座標 1 = y座標. + if ((ans_map[0]-range <= touchPos[0] && ans_map[0]+range >= touchPos[0]) && + (ans_map[1]-range <= touchPos[1] && ans_map[1]+range >= touchPos[1]) + ) + { + console.log(`answers \n +x${ans_map[0]+range},-x${ans_map[0]-range} \n +y${ans_map[1]+range},-y${ans_map[1]-range} \n`); + isTouchAnswer = true; + } + + else + { + // isTouchAnswer = false; + } + console.log(isTouchAnswer); + + + }) + + return isTouchAnswer; + } + + + function Drow_CorrectAnswer(){ + questionDate["answer_map"].forEach(ans_map => { + console.log("\n==============================================\n"); + Drow_ScuessImagePostion(ans_map); + }) + } + Drow_CorrectAnswer(); + + + +//============================================================================================================================================ +//============================================================================================================================================ +//============================================================================================================================================ + +//=======================イントロドン====================================== + + const introdon_startButton = document.getElementById('start-button'); + const submitButton = document.getElementById('submit-answer-button'); + const resultMessage = document.getElementById('result-message'); + // const nextButton = document.getElementById('next-button'); + const answerInput = document.getElementById('answer-input'); + const timeLeftElement = document.getElementById('time-left'); + const answerCountElement = document.getElementById('answer-count'); + const maxAnswerCountElement = document.getElementById('max-answer-count'); + const infoText = document.getElementById('info-text'); + const button1 = document.getElementById('button1'); + const button2 = document.getElementById('button2'); + + const audio = new Audio('your-music-file.mp3'); + let timer; + let timeRemaining = 10; + let answerCount = 0; + const maxAnswerCount = 5; + // let correctAnswer = "曲名"; + let quizFinished = false; + let answerEnabled = false; + + // 初期設定 + maxAnswerCountElement.textContent = maxAnswerCount; + answerInput.style.display = 'none'; + submitButton.style.display = 'none'; + submitButton.disabled = true; + + + introdon_startButton.addEventListener('click', function () { + audio.play(); + introdon_startButton.disabled = true; + startTimer(); + }); + + function startTimer() { + timer = setInterval(function () { + timeRemaining--; + timeLeftElement.textContent = timeRemaining; + + if (timeRemaining <= 0) { + clearInterval(timer); + endQuiz(false); + } + }, 1000); + } + + function resetButtons() { + button1.disabled = false; + button2.disabled = false; + button1.classList.remove('disabled'); + button2.classList.remove('disabled'); + button1.style.backgroundColor = '#4caf50'; + button2.style.backgroundColor = '#4caf50'; + } + + button1.addEventListener('click', function () { + enableAnswer(); + button1.disabled = true; + button2.classList.add('disabled'); + button2.style.backgroundColor = '#ccc'; + }); + + button2.addEventListener('click', function () { + enableAnswer(); + button2.disabled = true; + button1.classList.add('disabled'); + button1.style.backgroundColor = '#ccc'; + }); + + function enableAnswer() { + answerEnabled = true; + answerInput.style.display = 'block'; + submitButton.style.display = 'block'; + submitButton.disabled = false; + } + + submitButton.addEventListener('click', function () { + if (quizFinished || !answerEnabled) return; + + const userAnswer = answerInput.value.trim(); + answerCount++; + answerCountElement.textContent = answerCount; + + if (userAnswer.toLowerCase() === correctAnswer.toLowerCase()) { + endQuiz(true); + } else if (answerCount >= maxAnswerCount) { + endQuiz(false); + } else { + resultMessage.style.display = 'block'; + resultMessage.textContent = "×"; + resultMessage.className = 'incorrect'; + resetButtons(); // ボタンの状態をリセット + } + }); + + nextButton.addEventListener('click', function () { + // window.location.href = 'next-page.html'; + displayLoadRequest("home"); + }); + + function endQuiz(isCorrect) { + clearInterval(timer); + quizFinished = true; + + resultMessage.style.display = 'block'; + nextButton.style.display = 'block'; + + if (isCorrect) + { + resultMessage.textContent = "〇"; + resultMessage.className = 'correct'; + } + + else + { + resultMessage.textContent = "×"; + resultMessage.className = 'incorrect'; + } + + disableAllInputs(); + + // 解答入力と解答完了ボタンを無効化 + answerInput.disabled = true; + submitButton.disabled = true; + submitButton.classList.add('disabled'); + } + + function disableAllInputs() { + document.querySelectorAll('.answer-button').forEach(button => { + button.disabled = true; + button.classList.add('disabled'); + }); + + // 解答入力フィールドと解答完了ボタンを無効化 + answerInput.disabled = true; + submitButton.disabled = true; + submitButton.classList.add('disabled'); + } + + function enableAllInputs() { + document.querySelectorAll('.answer-button').forEach(button => { + button.disabled = false; + button.classList.remove('disabled'); + }); + // 解答完了ボタンも有効化 + submitButton.disabled = false; + submitButton.classList.remove('disabled'); + answerInput.disabled = false; + } + +//============================================================================================================================================ +//============================================================================================================================================ + + + console.log(gameKindOptions); + gameKindOptions.forEach(option => + { + console.log(option); + option.addEventListener("click", (event) => + { + const selectGameKind = event.target.textContent; + + //選択したボタンのtextCountentからと一致する名前のゲームのdivを表示する + if(gamekind[selectGameKind] != null){ + displayLoadRequest(gamekind[selectGameKind]); + } + + else + { + console.error(`selectGameKindOptionNothing ,date${selectGameKind}`); + } + }); + + + }); + + + // //テスト用のプログラムを組み込んでいる + // quiz_set(question_date[0]["question"], + // question_date[0]["selects"], + // question_date[0]["answer"]); + + + let quiz_number + // 次の問題へ(ここでは単純にホーム画面に戻る処理) + nextButton.addEventListener("click", () => { + displayLoadRequest("quiz"); + }); + + // やめる(ホーム画面に戻る) + quitButton.addEventListener("click", () => { + displayLoadRequest("home"); + }); + + + document.getElementById("submit_quiz").addEventListener("click", async function () { + // フォームからデータを取得 + const question = document.getElementById("quiz_question").value; + const options = [ + document.getElementById("option_1").value, + document.getElementById("option_2").value, + document.getElementById("option_3").value, + document.getElementById("option_4").value, + ]; + const answer = parseInt(document.getElementById("quiz_answer").value, 10); + + // 入力チェック + if (!question || options.some(option => !option) || isNaN(answer) || answer < 1 || answer > 4) { + alert("すべてのフィールドを正しく入力してください。"); + return; + } + + // クイズデータを表示(または送信) + const quizData = { + question: question, + selects: options, + answer_idx: answer, + }; + + // クイズデータをサーバーに送信 + try { + const response = await fetch('http://localhost:5001/api/create/quiz', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quizData), + }); + + if (!response.ok) { + throw new Error('Failed to fetch'); + } + + console.log(await response.json()); + } catch (error) { + console.error(error); + }; + + + console.log("クイズデータ:", quizData); + alert("クイズが作成されました!"); + + }); + + }); + + + \ No newline at end of file diff --git a/frontend/game.jsREADME.md b/frontend/game.jsREADME.md new file mode 100644 index 0000000..264c0c9 --- /dev/null +++ b/frontend/game.jsREADME.md @@ -0,0 +1,11 @@ +`displayLoadRequest` +どの画面を表示するかをstring側で受け取り。 +指定された画面を表示する. + +|key |場所 | +|:--|:--:| +|home |title画面 | +|quiz |クイズ(選択式) | +|game_mode |作成するのか遊ぶのかを選択する | +|game_select |ゲームの種類(クイズ、間違い探しなど)を選択する | +|result |リザルト(結果発表)画面 | diff --git a/frontend/games/FindDifference.js b/frontend/games/FindDifference.js new file mode 100644 index 0000000..0a596be --- /dev/null +++ b/frontend/games/FindDifference.js @@ -0,0 +1,182 @@ +const images_list = document.querySelectorAll('.different-image'); + + +const debug_log = { + "postion":false, + "isDevicePhone":false +} + + +//アクセスしているのがスマホかPCかを確認する +function isSmartPhone() +{ + if (navigator.userAgent.match(/iPhone|Android.+Mobile/)) + { + console.log(`UserAgent${navigator.userAgent}, phone:True`); + return true; + } + + else + { + console.log(`UserAgent${navigator.userAgent},phone:False`); + return false; + } +} + + +const isUsePhone = isSmartPhone(); + + + + + +//request-date + +/** + * @param ImageList srcに代入するためのURL-S3から引っ張ってくるためのキーを指定する + * @param answer_map クリックするとどこが間違ってるかを教えてくれる + * @param CorrectAnswer answer_mapの連番でどれが正解したかを保存する + */ + +let questionDate = { + "ImageList":[], + "answer_map":[[116,69],[42,51]], + "CorrectAnswer":[false,false], +} + + +const imgs = document.querySelectorAll(".imgs"); + +imgs.forEach(img =>{ + questionDate["ImageList"].push([img.src,img.naturalWidth,img.naturalHeight]); + +}) +console.log(questionDate["ImageList"]); +LoadImages(0,questionDate["ImageList"][0]); +LoadImages(1,questionDate["ImageList"][1]); + + + +let userDate = { + "lastTouchPos":[25.32, 3452.3], + "selectTouchPostions":[[2432,343],[2432,343]] +} + + +let userClick_history = []; +let canvas_history = [[],[]] +const canvas_images = document.querySelectorAll(".different-image"); + +/** + * 画像をロードする関数です。 + * @param images_number (int)連番の値 + * @param images_number ()連番の値 + */ +function LoadImages(images_number,LoadImage) +{ + // images_list[images_number].HTMLimages = LoadImage + var drowCTX = images_list[images_number].getContext('2d'); + + images_list[images_number].width = LoadImage[1].naturalWidth; // 元画像の幅 + images_list[images_number].height = LoadImage[2].naturalHeight; // 元画像の高さ + drowCTX.drawImage(LoadImage[0], 0, 0, LoadImage[1], LoadImage[2]); +} + + +console.log(images_list[0]); + +/** + * 指定した場所を描画する処理 + * @param map 一次元の配列です{34,42}のような形式で送ってください + */ +function Drow_ScuessImagePostion(map){ + images_list.forEach(img =>{ + var drowCtx = img.getContext("2d"); + drowCtx.beginPath(); + drowCtx.arc(map[0], map[1], 2, 0, Math.PI * 2); + + drowCtx.fillStyle = 'red'; + if(Check_TouchImagePostion_CorrectAnswer([map[0],map[1]],questionDate["answer_map"],10)) + { + drowCtx.fillStyle = 'green'; + } + + drowCtx.fill(); + drowCtx.stroke(); + }) +} + + + + + +//画像のどこをタップしたかを調べる処理 +images_list.forEach(image => +{ + image.addEventListener("click",(c)=> + { + console.log(c.offsetX,c.offsetY); + + + userDate["lastTouchPos"][0] = c.offsetX; + userDate["lastTouchPos"][1] = c.offsetY; + + Drow_ScuessImagePostion([c.offsetX,c.offsetY]); + if (debug_log["postion"] == true){ + console.log( + Check_TouchImagePostion_CorrectAnswer( + userDate["lastTouchPos"], + questionDate["answer_map"], + 10 + ) + + ); + } + + }); +}); + + +/** + * 正解の場所かどうかを判定する処理 + * @param touchPos 要素数2の1次元の配列 + * @param answer_map 間違い場所を2次元配列で管理しているもの + * @param range どれくらいのpx誤差を許容するか? + */ +function Check_TouchImagePostion_CorrectAnswer(touchPos,answer_map,range){ + let isTouchAnswer = false; + answer_map.forEach(ans_map => { + // isTouchAnswer = false; + console.log(`helloWOrld${ans_map}`) + console.log(`Check_touchPosCorrectAnswer\n SelectPos-x:${touchPos[0]},y:${touchPos[1]} \n AnswerPos-x:${ans_map[0]}, y:${ans_map[1]} \n range:${range}`); + + + //0 = x座標 1 = y座標. + if ((ans_map[0]-range <= touchPos[0] && ans_map[0]+range >= touchPos[0]) && + (ans_map[1]-range <= touchPos[1] && ans_map[1]+range >= touchPos[1]) + ) + { + console.log(`answers \n +x${ans_map[0]+range},-x${ans_map[0]-range} \n +y${ans_map[1]+range},-y${ans_map[1]-range} \n`); + isTouchAnswer = true; + } + + else + { + // isTouchAnswer = false; + } + console.log(isTouchAnswer); + + + }) + + return isTouchAnswer; +} + +function Drow_CorrectAnswer(){ + questionDate["answer_map"].forEach(ans_map => { + console.log("\n==============================================\n"); + Drow_ScuessImagePostion(ans_map); + }) +} +Drow_CorrectAnswer(); + diff --git a/frontend/games/introdon.js b/frontend/games/introdon.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/games/quiz.js b/frontend/games/quiz.js new file mode 100644 index 0000000..a949562 --- /dev/null +++ b/frontend/games/quiz.js @@ -0,0 +1,60 @@ +// console.log("loadQuiz.js"); + + + +// quiz +const select_options = document.querySelectorAll(".selects"); +const question = document.getElementById("question_text"); +let correctAnswer = "start-answer"; + + + /** + * 質問を書き込む + * @param question_text (string)質問文 + * @param selects (list-string)選択肢で表示するもの + * @param answer_number (int)selectsでの連番-何番目の選択肢を正解にするか、 + * その名前のdisplayを表示する + */ + function quiz_set(question_text,selects,answer_number) + { + question.textContent = question_text; + const quiz_contents = select_options.item(0).children; + for(var i = 0; i < 4;i++) + { + quiz_contents[i].textContent = selects[i]; + } + correctAnswer = selects[answer_number]; + } + + + function quiz_SelectEvent() + { + select_options.forEach(option => + { + option.addEventListener("click", (event) => { + const selectedAnswer = event.target.textContent; + + // 判定処理 + if (selectedAnswer === correctAnswer) + { + statusText.textContent = "正解"; + statusText.style.color = "#28a745"; + } + else + { + statusText.textContent = "不正解"; + statusText.style.color = "#dc3545"; + } + + answerText.textContent = correctAnswer; // 正解を表示 + + // 結果画面へ移動 + displayLoadRequest("result"); + }); + }); + } + +export { + quiz_SelectEvent, + quiz_set +} \ No newline at end of file diff --git a/frontend/image/ban-1.png b/frontend/image/ban-1.png new file mode 100644 index 0000000..6bbca84 Binary files /dev/null and b/frontend/image/ban-1.png differ diff --git a/frontend/image/ban-2.png b/frontend/image/ban-2.png new file mode 100644 index 0000000..4fbde36 Binary files /dev/null and b/frontend/image/ban-2.png differ diff --git a/frontend/image/fabicon.ico b/frontend/image/fabicon.ico new file mode 100644 index 0000000..b43f168 Binary files /dev/null and b/frontend/image/fabicon.ico differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9b188b8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,148 @@ + + + + + + クイズアプリ + + + + + +
+

ミニラン

+ +
+ + +
+
+ + + +
+
+ +
+
+ + +
+
+ + + + +
+ + +
+

間違い探し

+
+

05/??

+ + + + +
+
+ + +
+

クイズ

+

+
+ + + + +
+
+ + +
+

制限時間は10秒!5回まで答えられるよ!

+
+

残り時間: 10

+

解答回数: 0/5

+ +
+ + +
+ + + +
+
+ + + +
+

+
+ + +
+
+ +
+ +
+ + +
+ + + +
+ + +
+

間違い探し

+
+ 画像1を選択 + 画像2を選択 +
+
+ + +
+

クイズ作成

+
+
+ + +
+
+ + + + + +
+
+ + +
+ +
+
+ + + + +
+ +
+
+ + + + + + + + + + diff --git a/frontend/make.css b/frontend/make.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..544b7b4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..4f32952 --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,160 @@ +body { + font-family: 'Arial', sans-serif; + background-color: #f8f9fa; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + } + + .container { + max-width: 500px; + } + + .logo { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 20px; + } + + .title { + font-size: 1.8rem; + margin-bottom: 10px; + } + + .question { + font-size: 1.5rem; + margin: 20px 0; + } + + .question.h2{ + color: black; + } + + .options .btn { + width: 100%; + margin: 5px 0; + } + + .status { + font-size: 1.2rem; + color: #28a745; + } + + .answer { + font-size: 2rem; + font-weight: bold; + margin: 20px 0; + } + + .actions .btn { + width: 100%; + margin: 5px 0; + } + + .d-none { + display: none; + } + + + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +#quiz-container { + text-align: center; + color: #333; + width: 100%; + max-width: 500px; /* 適切な幅を指定 */ +} + +#info-text { + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; + color: #ff4081; +} + +button { + width: 120px; + height: 45px; + padding: 10px 20px; + margin: 10px 0; /* 垂直方向の間隔を調整 */ + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + transition: background-color 0.3s ease; +} + +#start-button { + background-color: #ff4081; + color: white; +} + +.answer-button { + background-color: #4caf50; + color: white; +} + +button.disabled { + background-color: #ccc; + cursor: not-allowed; +} + +#answer-input { + padding: 10px; + font-size: 16px; + border-radius: 5px; + border: 1px solid #ddd; + margin-top: 10px; + width: calc(100% - 40px); /* 入力フィールドの幅調整 */ + box-sizing: border-box; + display: none; +} + +#submit-answer-button { + background-color: #00bcd4; /* 水色 */ + color: white; + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin-top: 10px; +} + +button.disabled, +#answer-input.disabled { + background-color: #ccc; /* 灰色 */ + cursor: not-allowed; + color: #666; +} + +#next-button { + background-color: #ff9800; /* オレンジ */ + color: white; + margin-top: 20px; +} + +#result-message { + font-size: 100px; + font-weight: bold; + margin: 20px; +} + +.correct { + color: blue; +} + +.incorrect { + color: red; +} diff --git a/frontend/thinks/differentpoint.md b/frontend/thinks/differentpoint.md new file mode 100644 index 0000000..fb50a3b --- /dev/null +++ b/frontend/thinks/differentpoint.md @@ -0,0 +1,35 @@ +間違い探しの仕様 + +必要なもの + - 間違い探しの画像 + - 間違ってる部分のマップ(クリックした場所が間違っているかどうかを描画・判定する) + +処理 + + +Play + - GET_TouchImagePostion + 画像のどこをタップしたかを調べる処理 + | + V + addEventlistenerに変更した + + + - Drow_ScuessImagePostion + 正解の場所を描画する処理 + + + - Check_TouchImagePostion_CorrectAnswer + 正解の場所かどうかを判定する処理 + + +Make + - アップロードした画像を送信する + SendImage + + - アップロードした画像のプレビューを表示する + PreviewImage + + 挑戦 + プレービュー(テストプレー)できる状態 + TestPlay diff --git a/index.html b/index.html new file mode 100644 index 0000000..a4af0a7 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + + + イントロドン + + + +
+

制限時間は10秒!5回まで答えられるよ!

+
+

残り時間: 10

+

解答回数: 0/5

+ +
+ + +
+ + + + +
+
+ + + + diff --git a/index3.html b/index3.html new file mode 100644 index 0000000..5c57f6b --- /dev/null +++ b/index3.html @@ -0,0 +1,32 @@ + + + + + + ボタンで画面遷移 + + + +
+

ボタンで画面遷移

+
+ + + +
+
+ + + +
+
+ + + + diff --git a/make.css b/make.css new file mode 100644 index 0000000..5ca278a --- /dev/null +++ b/make.css @@ -0,0 +1,98 @@ +/* 画面全体の中央揃え */ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +#quiz-container { + text-align: center; + color: #333; + width: 100%; + max-width: 500px; /* 適切な幅を指定 */ +} + +#info-text { + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; + color: #ff4081; +} + +button { + padding: 10px 20px; + margin: 10px 0; /* 垂直方向の間隔を調整 */ + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + transition: background-color 0.3s ease; +} + +#start-button { + background-color: #ff4081; + color: white; +} + +.answer-button { + background-color: #4caf50; + color: white; +} + +button.disabled { + background-color: #ccc; + cursor: not-allowed; +} + +#answer-input { + padding: 10px; + font-size: 16px; + border-radius: 5px; + border: 1px solid #ddd; + margin-top: 10px; + width: calc(100% - 40px); /* 入力フィールドの幅調整 */ + box-sizing: border-box; + display: none; +} + +#submit-answer-button { + background-color: #00bcd4; /* 水色 */ + color: white; + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin-top: 10px; +} + +button.disabled, +#answer-input.disabled { + background-color: #ccc; /* 灰色 */ + cursor: not-allowed; + color: #666; +} + +#next-button { + background-color: #ff9800; /* オレンジ */ + color: white; + margin-top: 20px; +} + +#result-message { + font-size: 100px; + font-weight: bold; + margin: 20px; +} + +.correct { + color: blue; +} + +.incorrect { + color: red; +} \ No newline at end of file diff --git a/merged_index.html b/merged_index.html new file mode 100644 index 0000000..63b5dda --- /dev/null +++ b/merged_index.html @@ -0,0 +1,61 @@ + + + + + + イントロドン + + + + + + + + + + +
+

制限時間は60秒!5回まで答えられるよ!

+
+

残り時間: 60

+

解答回数: 0/5

+ +
+ + +
+ + + + +
+
+ +
+

イントロドン作成

+ + +

+ + +

+ + +
+ + + + \ No newline at end of file diff --git a/merged_script.js b/merged_script.js new file mode 100644 index 0000000..6f9df2c --- /dev/null +++ b/merged_script.js @@ -0,0 +1,233 @@ +document.addEventListener('DOMContentLoaded', function () { + // DOM要素の取得 + const startButton = document.getElementById('start-button'); + const submitButton = document.getElementById('submit-answer-button'); + const resultMessage = document.getElementById('result-message'); + const nextButton = document.getElementById('next-button'); + const answerInput = document.getElementById('answer-input'); + const timeLeftElement = document.getElementById('time-left'); + const answerCountElement = document.getElementById('answer-count'); + const maxAnswerCountElement = document.getElementById('max-answer-count'); + const infoText = document.getElementById('info-text'); + const button1 = document.getElementById('button1'); + const button2 = document.getElementById('button2'); + const createButton = document.getElementById('create-button'); + const audioUpload = document.getElementById('audio-upload'); + const statusMessage = document.getElementById('status-message'); + const navigation = document.getElementById("navigation"); + const quizContainer = document.getElementById("quiz-container"); + const createQuizContainer = document.getElementById("create-quiz-container"); + const showQuizButton = document.getElementById("show-quiz"); + const showCreateButton = document.getElementById("show-create"); + const screens = document.querySelectorAll(".screen"); + + + + + + // 画面切り替え関数 + function switchScreen(targetScreen) { + screens.forEach(screen => screen.classList.add("d-none")); // すべて非表示 + + + + + // 指定の画面を表示 + targetScreen.classList.remove("d-none"); + } + + + // イベントリスナーを追加 + showQuizButton.addEventListener("click", () => { + navigation.classList.add("d-none"); + quizContainer.classList.remove("d-none"); + createQuizContainer.classList.add("d-none"); + + + }); + + showCreateButton.addEventListener("click", () => { + navigation.classList.add("d-none"); + quizContainer.classList.add("d-none"); + createQuizContainer.classList.remove("d-none"); + + }); + + + }); + + // クイズ設定 + const audio = new Audio('your-music-file.mp3'); + let timer; + let timeRemaining = 60; + let answerCount = 0; + const maxAnswerCount = 5; + let correctAnswer = "曲名"; + let quizFinished = false; + let answerEnabled = false; + + // 音声アップロード初期設定 + if (createButton) createButton.disabled = true; + + + // 初期化 + maxAnswerCountElement.textContent = maxAnswerCount; + answerInput.style.display = 'none'; + submitButton.style.display = 'none'; + submitButton.disabled = true; + + + + + // クイズ開始 + startButton.addEventListener('click', function () { + audio.play(); + startButton.disabled = true; + startTimer(); + }); + + function startTimer() { + timer = setInterval(function () { + timeRemaining--; + timeLeftElement.textContent = timeRemaining; + + if (timeRemaining <= 0) { + clearInterval(timer); + endQuiz(false); + } + }, 1000); + } + + function resetButtons() { + button1.disabled = false; + button2.disabled = false; + button1.classList.remove('disabled'); + button2.classList.remove('disabled'); + button1.style.backgroundColor = '#4caf50'; + button2.style.backgroundColor = '#4caf50'; + } + + button1.addEventListener('click', function () { + enableAnswer(); + button1.disabled = true; + button2.classList.add('disabled'); + button2.style.backgroundColor = '#ccc'; + }); + + button2.addEventListener('click', function () { + enableAnswer(); + button2.disabled = true; + button1.classList.add('disabled'); + button1.style.backgroundColor = '#ccc'; + }); + + function enableAnswer() { + answerEnabled = true; + answerInput.style.display = 'block'; + submitButton.style.display = 'block'; + submitButton.disabled = false; + } + + submitButton.addEventListener('click', function () { + if (quizFinished || !answerEnabled) return; + + const userAnswer = answerInput.value.trim(); + answerCount++; + answerCountElement.textContent = answerCount; + + if (userAnswer.toLowerCase() === correctAnswer.toLowerCase()) { + endQuiz(true); + } else if (answerCount >= maxAnswerCount) { + endQuiz(false); + } else { + resultMessage.style.display = 'block'; + resultMessage.textContent = "×"; + resultMessage.className = 'incorrect'; + resetButtons(); + } + }); + + nextButton.addEventListener('click', function () { + window.location.href = 'next-page.html'; + }); + + function endQuiz(isCorrect) { + clearInterval(timer); + quizFinished = true; + + resultMessage.style.display = 'block'; + nextButton.style.display = 'block'; + + if (isCorrect) { + resultMessage.textContent = "〇正解"; + resultMessage.className = 'correct'; + } else { + resultMessage.textContent = "×不正解"; + resultMessage.className = 'incorrect'; + } + + disableAllInputs(); + answerInput.disabled = true; + submitButton.disabled = true; + submitButton.classList.add('disabled'); + } + + function disableAllInputs() { + document.querySelectorAll('.answer-button').forEach(button => { + button.disabled = true; + button.classList.add('disabled'); + }); + answerInput.disabled = true; + submitButton.disabled = true; + submitButton.classList.add('disabled'); + } + + // 音声アップロードの処理 + if (audioUpload) { + audioUpload.addEventListener('change', toggleCreateButton); + answerInput.addEventListener('input', toggleCreateButton); + } + + function toggleCreateButton() { + const audioFile = audioUpload.files[0]; + const answer = answerInput.value.trim; + + if (createButton) { + createButton.disabled = !(audioFile && answer&& audioFile.type.startsWith("audio")); + } + } + + if (createButton) { + createButton.addEventListener('click', function () { + const audioFile = audioUpload.files[0]; + const answer = answerInput.value; + + if (!audioFile || !answer) { + alert("音声ファイルと答えを入力してください!"); + return; + } + + const formData = new FormData(); + formData.append("audio", audioFile); + formData.append("answer", answer); + + fetch("YOUR_BACKEND_URL", { + method: "POST", + body: formData + }) + .then((response) => { + if (response.ok) { + statusMessage.style.display = "block"; + } else { + alert("エラーが発生しました。再試行してください。"); + } + }) + .catch((error) => { + console.error("通信エラー:", error); + alert("通信エラーが発生しました。"); + }); + }); + } + + + diff --git a/merged_style.css b/merged_style.css new file mode 100644 index 0000000..4529889 --- /dev/null +++ b/merged_style.css @@ -0,0 +1,118 @@ +/* 全体のスタイル */ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #fef5e7; /* 明るいクリーム色 */ +} + +/* コンテナスタイル */ +#create-quiz-container, #quiz-container { + text-align: center; + background-color: #ffffff; + border-radius: 15px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 30px; + width: 400px; + max-width: 500px; /* 適切な幅を指定 */ +} + +/* タイトルのスタイル */ +h1 { + font-size: 24px; + color: #ff6f61; /* 明るいピンク */ + margin-bottom: 20px; +} + +/* ステータスメッセージ */ +#info-text, #status-message { + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; + color: #ff4081; +} + +/* ボタンスタイル */ +button { + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin: 10px 0; + background-color: #4caf50; /* 明るい緑 */ + color: white; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +button:hover { + background-color: #45a049; /* 濃い緑 */ + transform: scale(1.05); +} + +button.disabled { + background-color: #ccc; + cursor: not-allowed; + color: #666; +} + +/* クイズ関連ボタン */ +#start-button { + background-color: #ff4081; + color: white; +} + +.answer-button { + background-color: #4caf50; + color: white; +} + +#submit-answer-button { + background-color: #00bcd4; /* 水色 */ + color: white; + margin-top: 10px; +} + +#next-button { + background-color: #ff9800; /* オレンジ */ + color: white; + margin-top: 20px; +} + +/* 入力フィールドのスタイル */ +input[type="text"], input[type="file"], #answer-input { + padding: 10px; + font-size: 16px; + border-radius: 5px; + border: 2px solid #ddd; + margin-bottom: 20px; + width: calc(100% - 20px); /* 入力フィールドの幅調整 */ + box-sizing: border-box; +} + +#answer-input { + display: none; +} + +/* 結果メッセージ */ +#result-message { + font-size: 100px; + font-weight: bold; + margin: 20px; +} + +.correct { + color: blue; +} + +.incorrect { + color: red; +} + +.d-none { + display: none; +} + diff --git a/play.css b/play.css new file mode 100644 index 0000000..e69de29 diff --git a/script3.js b/script3.js new file mode 100644 index 0000000..25e0747 --- /dev/null +++ b/script3.js @@ -0,0 +1,22 @@ +function navigate(screenNumber) { + // すべての画面を非表示にする + document.getElementById('screen1').style.display = 'none'; + document.getElementById('screen2').style.display = 'none'; + document.getElementById('screen3').style.display = 'none'; + + // 選択された画面を表示 + document.getElementById('screen' + screenNumber).style.display = 'block'; + + // ボタンをリセットして灰色にする + resetButtons(); + + // 選択されたボタン以外を無効化(灰色にする) + document.getElementById('btn' + screenNumber).classList.add('inactive-btn'); +} + +function resetButtons() { + // すべてのボタンを元に戻す + document.getElementById('btn1').classList.remove('inactive-btn'); + document.getElementById('btn2').classList.remove('inactive-btn'); + document.getElementById('btn3').classList.remove('inactive-btn'); +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..bc42461 --- /dev/null +++ b/style.css @@ -0,0 +1,99 @@ + +/* 画面全体の中央揃え */ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #f0f0f0; +} + +#quiz-container { + text-align: center; + color: #333; + width: 100%; + max-width: 500px; /* 適切な幅を指定 */ +} + +#info-text { + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; + color: #ff4081; +} + +button { + padding: 10px 20px; + margin: 10px 0; /* 垂直方向の間隔を調整 */ + font-size: 16px; + cursor: pointer; + border-radius: 5px; + border: none; + transition: background-color 0.3s ease; +} + +#start-button { + background-color: #ff4081; + color: white; +} + +.answer-button { + background-color: #4caf50; + color: white; +} + +button.disabled { + background-color: #ccc; + cursor: not-allowed; +} + +#answer-input { + padding: 10px; + font-size: 16px; + border-radius: 5px; + border: 1px solid #ddd; + margin-top: 10px; + width: calc(100% - 40px); /* 入力フィールドの幅調整 */ + box-sizing: border-box; + display: none; +} + +#submit-answer-button { + background-color: #00bcd4; /* 水色 */ + color: white; + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin-top: 10px; +} + +button.disabled, +#answer-input.disabled { + background-color: #ccc; /* 灰色 */ + cursor: not-allowed; + color: #666; +} + +#next-button { + background-color: #ff9800; /* オレンジ */ + color: white; + margin-top: 20px; +} + +#result-message { + font-size: 100px; + font-weight: bold; + margin: 20px; +} + +.correct { + color: blue; +} + +.incorrect { + color: red; +} diff --git a/style2.css b/style2.css new file mode 100644 index 0000000..a49d54a --- /dev/null +++ b/style2.css @@ -0,0 +1,62 @@ + +/* 全体のスタイル */ +body { + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + background-color: #fef5e7; /* 明るいクリーム色 */ +} + +/* コンテナのスタイル */ +#create-quiz-container { + text-align: center; + background-color: #ffffff; + border-radius: 15px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 30px; + width: 400px; +} + +/* タイトルのスタイル */ +h1 { + font-size: 24px; + color: #ff6f61; /* 明るいピンク */ + margin-bottom: 20px; +} + +/* ボタンスタイル */ +button { + padding: 10px 20px; + font-size: 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin: 10px 0; + background-color: #4caf50; /* 明るい緑 */ + color: white; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +button:hover { + background-color: #45a049; /* 濃い緑 */ + transform: scale(1.05); +} + +/* 入力フィールドのスタイル */ +input[type="text"], input[type="file"] { + padding: 10px; + font-size: 16px; + border: 2px solid #ddd; + border-radius: 5px; + margin-bottom: 20px; + width: calc(100% - 20px); +} + +/* ステータスメッセージ */ +#status-message { + color: #4caf50; /* 成功メッセージの緑 */ + font-weight: bold; +} diff --git a/style3.css b/style3.css new file mode 100644 index 0000000..06a5129 --- /dev/null +++ b/style3.css @@ -0,0 +1,37 @@ +body { + background-color: #fef3c7; /* クリーム色 */ + font-family: 'Arial', sans-serif; +} + +.container { + max-width: 600px; + margin-top: 50px; +} + +h2 { + font-size: 2em; + color: #ff6f61; /* ポップなピンク色 */ +} + +button { + font-size: 1.2em; + margin: 10px; + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s; +} + +button:hover { + background-color: #ffb6b6; /* ホバー時に明るいピンク色 */ +} + +.inactive-btn { + background-color: #d6d6d6; /* 灰色 */ + pointer-events: none; +} + +.screen { + margin-top: 20px; +} diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..e69de29 diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..0da8f53 --- /dev/null +++ b/test.txt @@ -0,0 +1,33 @@ +{ + "code": 200, + "game": { + "question": { + "S": "次のうち、太陽系に存在する惑星はどれですか?" + }, + "likes": { + "N": "0" + }, + "answer_idx": { + "N": "2" + }, + "id": { + "N": "3" + }, + "selects": { + "L": [ + { + "S": "シリウス" + }, + { + "S": "地球" + }, + { + "S": "アンドロメダ" + }, + { + "S": "アルタイル" + } + ] + } + } +}