├── backend ├── .gitignore ├── sample.env ├── requirements.txt ├── config.py ├── README.md ├── LICENSE ├── Models.py └── app.py ├── frontend ├── public │ ├── github.png │ └── vite.svg ├── src │ ├── icons │ │ ├── image.jpg │ │ └── fragments-svgrepo-com.svg │ ├── App.jsx │ ├── main.jsx │ ├── index.css │ ├── App.css │ ├── assets │ │ └── react.svg │ └── Home.jsx ├── postcss.config.js ├── README.md ├── vite.config.js ├── tailwind.config.js ├── index.html ├── .eslintrc.cjs └── package.json ├── pixel_update.json ├── .gitignore ├── README.md ├── validate_pixel_update.py └── .github └── workflows └── main.yml /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv/ 3 | __pycache__/ -------------------------------------------------------------------------------- /frontend/public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amfoss/amplace/HEAD/frontend/public/github.png -------------------------------------------------------------------------------- /frontend/src/icons/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amfoss/amplace/HEAD/frontend/src/icons/image.jpg -------------------------------------------------------------------------------- /backend/sample.env: -------------------------------------------------------------------------------- 1 | MODE="" 2 | 3 | DB_USERNAME="" 4 | DB_PASSWORD="" 5 | DB_NAME="" 6 | 7 | APP_SECRET_KEY="" -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend for am/Place 2 | Replace the API URL's with the hosted backends url 3 | 4 | Author: [@JATAYU000](https://github.com/JATAYU000) 5 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import Home from "./Home"; 2 | 3 | function App() { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,jsx,ts,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.8.2 2 | click==8.1.7 3 | PyMySQL==1.1.1 4 | Flask==3.0.3 5 | Flask-Cors==5.0.0 6 | greenlet==3.1.1 7 | itsdangerous==2.2.0 8 | Jinja2==3.1.4 9 | MarkupSafe==3.0.0 10 | python-dotenv==1.0.1 11 | SQLAlchemy==2.0.35 12 | typing_extensions==4.12.2 13 | Werkzeug==3.0.4 14 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | amPlace 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pixel_update.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": "0", 4 | "y": "0", 5 | "rgb": "#ffffff" 6 | }, 7 | { 8 | "x": "1", 9 | "y": "0", 10 | "rgb": "#ffffff" 11 | }, 12 | { 13 | "x": "2", 14 | "y": "0", 15 | "rgb": "#ffffff" 16 | }, 17 | { 18 | "x": "3", 19 | "y": "0", 20 | "rgb": "#ffffff" 21 | }, 22 | { 23 | "x": "4", 24 | "y": "0", 25 | "rgb": "#ffffff" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | curr_env = os.environ["MODE"] if "MODE" in os.environ else "development" 7 | DB_URL = "" if curr_env == "production" else "localhost" 8 | DB_USERNAME = os.getenv("DB_USERNAME") 9 | DB_PASSWORD = os.getenv("DB_PASSWORD") 10 | DB_NAME = os.getenv("DB_NAME") 11 | TIMEZONE = os.getenv("TIMEZONE") 12 | 13 | APP_SECRET_KEY = os.getenv("APP_SECRET_KEY") 14 | 15 | config = { 16 | "SQL_URI": f"mariadb+pymysql://{DB_USERNAME}:{DB_PASSWORD}@{DB_URL}:3306/{DB_NAME}", 17 | "TIMEZONE": TIMEZONE, 18 | "APP_SECRET_KEY" : APP_SECRET_KEY 19 | } 20 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Create an SVG-based cursor using the paintbrush emoji */ 6 | /* Apply the rotated paintbrush emoji as a cursor */ 7 | body { 8 | cursor: url('data:image/svg+xml;utf8,🖌️'), auto; 9 | background-image: url('./icons/image.jpg'); /* Path to your background image */ 10 | background-size: cover; /* Ensures the image covers the entire area */ 11 | background-position: center; /* Centers the image */ 12 | background-repeat: repeat; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | frontend/.DS_Store 2 | frontend/node_modules 3 | frontend/*.log 4 | frontend/explorations 5 | frontend/TODOs.md 6 | frontend/RELEASE_NOTE*.md 7 | frontend/packages/server-renderer/basic.js 8 | frontend/packages/server-renderer/build.dev.js 9 | frontend/packages/server-renderer/build.prod.js 10 | frontend/packages/server-renderer/server-plugin.js 11 | frontend/packages/server-renderer/client-plugin.js 12 | frontend/packages/template-compiler/build.js 13 | frontend/packages/template-compiler/browser.js 14 | frontend/.vscode 15 | frontend/dist 16 | frontend/temp 17 | frontend/types/v3-generated.d.ts 18 | 19 | backend/.idea/ 20 | backend/.venv*/ 21 | backend/venv*/ 22 | backend/__pycache__/ 23 | backend/dist/ 24 | backend/.coverage* 25 | backend/htmlcov/ 26 | backend/.tox/ 27 | backend/docs/_build/ 28 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.3.3", 18 | "@types/react-dom": "^18.3.0", 19 | "@vitejs/plugin-react": "^4.3.1", 20 | "autoprefixer": "^10.4.19", 21 | "eslint": "^8.57.0", 22 | "eslint-plugin-react": "^7.34.2", 23 | "eslint-plugin-react-hooks": "^4.6.2", 24 | "eslint-plugin-react-refresh": "^0.4.7", 25 | "postcss": "^8.4.39", 26 | "tailwindcss": "^3.4.4", 27 | "vite": "^5.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## delete pixel 2 | DELETE http://localhost:5000/api/delete_pixel 3 | Content-Type: application/json 4 | 5 | { "X":9, 6 | "Y":15 7 | } 8 | 9 | ## Test get user details 10 | GET http://localhost:5000/api/get_user_details 11 | 12 | ## Test updating pixels 13 | POST http://localhost:5000/api/update_pixel 14 | Content-Type: application/json 15 | 16 | { "user" : "test_user", 17 | "pixel_list": [ 18 | { 19 | "X": 10, 20 | "Y": 15, 21 | "hex-code": "#ff5733" 22 | }, 23 | { 24 | "X": 12, 25 | "Y": 18, 26 | "hex-code": "#33c5ff" 27 | }, 28 | { 29 | "X": 10, 30 | "Y": 15, 31 | "hex-code": "#123456" 32 | }, 33 | { 34 | "X": 9, 35 | "Y": 15, 36 | "hex-code": "#123456" 37 | } 38 | ] 39 | } 40 | 41 | ## Test getting pixel details 42 | GET http://localhost:5000/api/get_pixel 43 | 44 | --- 45 | Author: [@Rihaan B H](https://github.com/RihaanBH-1810) 46 | -------------------------------------------------------------------------------- /frontend/src/icons/fragments-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rihaan B H 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/Models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from sqlalchemy import ( 5 | Boolean, 6 | Column, 7 | DateTime, 8 | ForeignKey, 9 | Integer, 10 | SmallInteger, 11 | String, 12 | Table, 13 | create_engine, 14 | ) 15 | from sqlalchemy.orm import declarative_base, relationship, sessionmaker 16 | 17 | from config import config, curr_env 18 | 19 | engine = create_engine(config["SQL_URI"]) 20 | Session = sessionmaker(bind=engine) 21 | Base = declarative_base() 22 | 23 | 24 | @dataclass 25 | class Pixel(Base): 26 | __tablename__ = "pixel" 27 | pid = Column(Integer, primary_key=True, autoincrement=True) 28 | x: int = Column(SmallInteger, default=None) 29 | y: int = Column(SmallInteger, default=None) 30 | color_hex: str = Column(String(30), default="#ffffff") 31 | user_id: str = Column(String(50), ForeignKey("user.username"), nullable=False) 32 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 33 | user = relationship("User", back_populates="pixels") 34 | 35 | @dataclass 36 | class User(Base): 37 | __tablename__ = "user" 38 | username: str = Column(String(50), primary_key=True) 39 | count: int = Column(Integer, default=0) 40 | 41 | pixels = relationship("Pixel", back_populates="user") 42 | 43 | Base.metadata.create_all(engine) 44 | 45 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # am/place 2 | 3 | am/place is an interactive game inspired by [r/place](https://en.wikipedia.org/wiki/R/place) built by amFOSS members for Hacktoberfest 2024. The way it works is that players submit PR's to this repository modifying the `pixel_update.json` file, the modifications to this file will reflect on pixels on the cavas. 4 | 5 | `pixel_update.json`: 6 | ```json 7 | [ 8 | { 9 | "x": "69", 10 | "y": "42", 11 | "rgb": "#8008ff" 12 | }, 13 | { 14 | "x": "70", 15 | "y": "7", 16 | "rgb": "#222222" 17 | }, 18 | { 19 | "x": "71", 20 | "y": "8", 21 | "rgb": "#333333" 22 | }, 23 | { 24 | "x": "72", 25 | "y": "9", 26 | "rgb": "#444444" 27 | }, 28 | { 29 | "x": "68", 30 | "y": "5", 31 | "rgb": "#555555" 32 | } 33 | ] 34 | ``` 35 | 36 | Each entry in this json list corresponds to a singular pixel on the canvas. There are a few rules to be followed when making PR's: 37 | 1. Only `pixel_update.json` should have modifications, you can verify this by running `git diff` before commiting. 38 | 2. The above prescribed format must be strictly followed. 39 | 3. You're only allowed to modify 5 pixels at a time. 40 | 41 | Failure to follow any of the above rules will result in your PR getting disqualified, we have setup a Github action which accepts and rejects PR's automagically. You can try out this game at [amplace.amfoss.in](https://amplace.amfoss.in) 42 | 43 | ## 2024 Canvas: 44 | 2603 PRs were made by 200+ participants over the course of 5 days. 45 | ![image](https://github.com/user-attachments/assets/e3cb0ab1-8a1f-41dc-b629-3b173ea829fc) 46 | 47 | 48 | ## Credits: 49 | Backend Author: [@Rihaan B H](https://github.com/RihaanBH-1810) 50 | 51 | Frontend Author: [@JATAYU000](https://github.com/JATAYU000) 52 | 53 | Misc Fixes: [@Hridesh MG](https://github.com/hrideshmg) 54 | -------------------------------------------------------------------------------- /validate_pixel_update.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import re 4 | import requests 5 | 6 | diff_file = sys.argv[1] 7 | content_file = sys.argv[2] 8 | github_user = sys.argv[3] 9 | api_url = sys.argv[4] 10 | 11 | def is_valid_rgb(value): 12 | hex_pattern = r'^#[0-9A-Fa-f]{6}$' 13 | return bool(re.match(hex_pattern, value)) 14 | 15 | def validate_dict(item): 16 | if not isinstance(item, dict): 17 | return False, "Item is not a dictionary." 18 | print(item) 19 | required_keys = {"x", "y", "rgb"} 20 | if set(item.keys()) != required_keys: 21 | return False, f"Item keys do not match the required keys {required_keys}." 22 | 23 | if not str(item['x']).isdigit() or not (0 <= int(item['x']) <= 149): 24 | return False, "Invalid 'x' value. Must be an integer between 0 and 149." 25 | 26 | if not str(item['y']).isdigit() or not (0 <= int(item['y']) <= 79): 27 | return False, "Invalid 'y' value. Must be an integer between 0 and 79." 28 | 29 | if not is_valid_rgb(item['rgb']): 30 | return False, "Invalid 'rgb' value. Must be a valid hex color (e.g., #ffffff)." 31 | 32 | return True, None 33 | 34 | with open(diff_file, 'r') as f: 35 | changes = f.readlines() 36 | 37 | print("Changes from the PR:") 38 | for change in changes: 39 | print(change) 40 | 41 | with open(content_file, 'r') as f: 42 | file_content = f.read() 43 | 44 | print("Content of the modified file (pixel_update.json):") 45 | print(file_content) 46 | 47 | try: 48 | data = json.loads(file_content) 49 | except json.JSONDecodeError as e: 50 | print(f"Error: Failed to parse JSON. {str(e)}") 51 | sys.exit(1) 52 | 53 | if not isinstance(data, list): 54 | print("Error: The content should be a list of dictionaries.") 55 | sys.exit(1) 56 | 57 | if len(data) > 5: 58 | print("Error: The list contains more than 5 dictionaries.") 59 | sys.exit(1) 60 | 61 | for i, item in enumerate(data): 62 | is_valid, error_message = validate_dict(item) 63 | if not is_valid: 64 | print(f"Error in dictionary at index {i}: {error_message}") 65 | sys.exit(1) 66 | 67 | print("Validation successful: The pixel_update.json file is correctly formatted.") 68 | 69 | pixel_list = [] 70 | for item in data: 71 | pixel_list.append({ 72 | "X": int(item['x']), 73 | "Y": int(item['y']), 74 | "hex-code": item['rgb'] 75 | }) 76 | 77 | post_data = { 78 | "user": github_user, 79 | "pixel_list": pixel_list 80 | } 81 | 82 | try: 83 | response = requests.post(api_url, json=post_data) 84 | response.raise_for_status() 85 | print("POST request successful.") 86 | print(f"Response: {response.json()}") 87 | except requests.exceptions.RequestException as e: 88 | print(f"Error: Failed to send POST request. {str(e)}") 89 | sys.exit(1) 90 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Validate/Merge 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | 14 | jobs: 15 | audit: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | PR_AUTHOR: ${{ steps.pr_user.outputs.PR_AUTHOR }} 19 | 20 | steps: 21 | - name: Check if PR is from a fork 22 | id: pr_source 23 | run: | 24 | if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then 25 | echo "PR is from a fork." 26 | echo "::set-output name=is_fork::true" 27 | else 28 | echo "PR is not from a fork." 29 | echo "::set-output name=is_fork::false" 30 | fi 31 | 32 | - name: Checkout base branch 33 | uses: actions/checkout@v4 34 | with: 35 | ref: ${{ github.event.pull_request.head.ref }} 36 | # Fetch the forked repository where the pull request originates 37 | repository: ${{ github.event.pull_request.head.repo.full_name }} 38 | fetch-depth: 0 39 | 40 | - name: Get the changed files 41 | id: changes 42 | run: | 43 | echo "Fetching changed files from GitHub API..." 44 | PR_NUMBER=${{ github.event.pull_request.number }} 45 | REPO=${{ github.repository }} 46 | echo "Fetching files changed in PR #$PR_NUMBER" 47 | 48 | # Call the GitHub API to get the list of changed files 49 | curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 50 | "https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/files" \ 51 | | jq -r '.[].filename' > changes.txt 52 | 53 | echo "Changed files:" 54 | cat changes.txt 55 | 56 | - name: Validate pixel_update.json 57 | run: | 58 | CHANGED_FILES=$(cat changes.txt) 59 | TOTAL_FILES=$(echo "$CHANGED_FILES" | wc -l) 60 | 61 | # Check if only 'pixel_update.json' was modified 62 | if [[ "$TOTAL_FILES" -eq 1 ]] && [[ "$CHANGED_FILES" == "pixel_update.json" ]]; then 63 | echo "Only pixel_update.json was modified. Proceeding with validation..." 64 | else 65 | echo "Error: Changes found in files other than pixel_update.json. Failing the PR." 66 | exit 1 67 | fi 68 | 69 | - name: Save the content of changed file 70 | run: | 71 | cat pixel_update.json > changed_file_content.txt 72 | 73 | - name: Get the PR author username 74 | id: pr_user 75 | run: | 76 | branch_name=${{ github.event.pull_request.user.login }} 77 | echo "PR_AUTHOR=$branch_name" >> $GITHUB_OUTPUT 78 | echo "The PR was created by $branch_name" 79 | 80 | - name: Pass the diff and file content to Python validation and send request 81 | run: | 82 | python validate_pixel_update.py changes.txt changed_file_content.txt ${{ github.event.pull_request.user.login }} https://amplacebackend.amfoss.in/api/update_pixel 83 | 84 | auto_merge_pr: 85 | needs: audit 86 | runs-on: ubuntu-latest 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | PR_AUTHOR: ${{ needs.audit.outputs.PR_AUTHOR }} 90 | PR_NUMBER: ${{ github.event.number }} 91 | 92 | steps: 93 | - name: Checkout Repository 94 | uses: actions/checkout@v4 95 | 96 | - name: Create branch if not exists 97 | run: | 98 | branch_name=$PR_AUTHOR 99 | if git ls-remote --heads origin $branch_name | grep -sw $branch_name; then 100 | echo "Branch $branch_name already exists." 101 | else 102 | echo "Branch $branch_name does not exist. Creating new branch..." 103 | git checkout -b $branch_name 104 | git push --set-upstream origin $branch_name 105 | fi 106 | 107 | - name: Change base branch of PR 108 | run: gh pr edit $PR_NUMBER --base $PR_AUTHOR 109 | 110 | - name: Merge PR 111 | run: gh pr merge $PR_NUMBER --merge --delete-branch --auto 112 | 113 | - name: Close PR if previous steps failed 114 | if: failure() 115 | run: | 116 | echo "Previous steps failed. Attempting to close PR #$PR_NUMBER" 117 | gh pr close $PR_NUMBER -R amfoss/amplace -d || echo "PR already merged or not closable, ignoring." 118 | -------------------------------------------------------------------------------- /backend/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from Models import Session, Pixel, User 3 | from config import config 4 | from datetime import datetime, timedelta 5 | from flask_cors import CORS, cross_origin 6 | app = Flask(__name__) 7 | cors = CORS(app) 8 | app.config['CORS_HEADERS'] = 'Content-Type' 9 | 10 | 11 | app.secret_key = config["APP_SECRET_KEY"] 12 | 13 | def check_cooldown(pixel): 14 | session = Session() 15 | try: 16 | flag = True 17 | existing_pixel = session.query(Pixel).filter_by(x=pixel["X"], y=pixel["Y"]).order_by(Pixel.updated_at.desc()).first() 18 | 19 | if existing_pixel: 20 | last_updated_time = existing_pixel.updated_at 21 | if last_updated_time > datetime.utcnow() - timedelta(minutes=2): 22 | flag = False 23 | 24 | finally: 25 | session.close() 26 | return flag 27 | 28 | @app.route("/api/update_pixel", methods=['POST']) 29 | @cross_origin() 30 | def update_pixel(): 31 | if request.method == "POST": 32 | try: 33 | cooldown_pixels = [] 34 | data = request.json 35 | session = Session() 36 | pixel_list = data["pixel_list"] 37 | username = data["user"] 38 | 39 | for pixel_data in pixel_list: 40 | 41 | user = session.query(User).filter_by(username=username).first() 42 | if not user: 43 | user = User(username=username) 44 | session.add(user) 45 | session.commit() 46 | 47 | existing_pixel = session.query(Pixel).filter_by(x=pixel_data["X"], y=pixel_data["Y"]).first() 48 | 49 | if existing_pixel: 50 | if check_cooldown(pixel_data): 51 | existing_pixel.color_hex = pixel_data["hex-code"] 52 | existing_pixel.updated_at = datetime.utcnow() 53 | existing_pixel.user = user 54 | user.count += 1 55 | session.add(user) 56 | else: 57 | cooldown_pixels.append(pixel_data) 58 | 59 | else: 60 | new_pixel = Pixel( 61 | user_id=user.username, 62 | x=pixel_data["X"], 63 | y=pixel_data["Y"], 64 | color_hex=pixel_data["hex-code"], 65 | updated_at=datetime.utcnow() 66 | ) 67 | user.count += 1 68 | 69 | session.add(user) 70 | session.add(new_pixel) 71 | session.commit() 72 | if len(cooldown_pixels) == 0: 73 | return jsonify({"success": True, "message" : "All pixels updated none blocked by a cooldown"}), 200 74 | elif len(cooldown_pixels) == len(pixel_list): 75 | return jsonify({"success" : True, "message": "All the pixels you tried to update have a cooldown try again in 2 min"}), 200 76 | else: 77 | return jsonify({"success" : True, "message" : f" the following pixels that you tried to update have a cooldown {cooldown_pixels}"}), 200 78 | 79 | except Exception as e: 80 | session.rollback() 81 | return jsonify({"success": False, "message": str(e)}), 404 82 | finally: 83 | session.close() 84 | 85 | @app.route("/api/get_pixel", methods=["GET"]) 86 | def get_pixel_details(): 87 | if request.method == "GET": 88 | try: 89 | session = Session() 90 | pixels = session.query(Pixel).all() 91 | 92 | pixel_list = [ 93 | { 94 | "user": pixel.user_id, 95 | "X": pixel.x, 96 | "Y": pixel.y, 97 | "hex-code": pixel.color_hex, 98 | "updated_at": pixel.updated_at.strftime("%Y-%m-%d %H:%M:%S") 99 | } 100 | for pixel in pixels 101 | ] 102 | 103 | return jsonify({"success": True, "pixels": pixel_list}), 200 104 | 105 | except Exception as e: 106 | return jsonify({"success": False, "message": str(e)}), 404 107 | 108 | finally: 109 | session.close() 110 | 111 | @app.route("/api/get_user_details", methods=["GET"]) 112 | def get_user_details(): 113 | if request.method == "GET": 114 | try: 115 | session = Session() 116 | users = session.query(User).all() 117 | 118 | users_data_list = [ 119 | { 120 | "user" : user.username, 121 | "score" : user.count 122 | } 123 | for user in users 124 | ] 125 | return jsonify({"success": True, "user_data": users_data_list}), 200 126 | 127 | except Exception as e: 128 | return jsonify({"success": False, "message": str(e)}), 404 129 | 130 | finally: 131 | session.close() 132 | 133 | @app.route("/api/delete_pixel", methods=['DELETE']) 134 | def delete_pixel(): 135 | if request.method == "DELETE": 136 | try: 137 | data = request.json 138 | session = Session() 139 | x = data.get("X") 140 | y = data.get("Y") 141 | if x is None or y is None: 142 | return jsonify({"success": False, "message": "X and Y coordinates are required"}), 400 143 | 144 | pixel = session.query(Pixel).filter_by(x=x, y=y).first() 145 | 146 | if not pixel: 147 | return jsonify({"success": False, "message": "Pixel not found"}), 404 148 | 149 | session.delete(pixel) 150 | session.commit() 151 | 152 | return jsonify({"success": True, "message": "Pixel deleted successfully"}), 200 153 | 154 | except Exception as e: 155 | session.rollback() 156 | return jsonify({"success": False, "message": str(e)}), 500 157 | 158 | finally: 159 | session.close() 160 | 161 | 162 | if __name__ == "__main__": 163 | app.run(debug=True) 164 | -------------------------------------------------------------------------------- /frontend/src/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | const Home = () => { 4 | const mainCanvasRef = useRef(null); 5 | const overlayCanvasRef = useRef(null); 6 | const [info, setInfo] = useState('Clicked Box: (x, y)'); 7 | const [highlightedPixel, setHighlightedPixel] = useState(null); 8 | const [showLeaderboard, setShowLeaderboard] = useState(false); 9 | const [leaderboardData, setLeaderboardData] = useState([]); 10 | const overlayPositionRef = useRef({ top: 0, left: 0 }); 11 | 12 | const cols = 150; 13 | const rows = 80; 14 | const boxSize = 10; 15 | 16 | const [pixel_db, setPixelDB] = useState([]); 17 | 18 | const fetchPixelData = async () => { 19 | try { 20 | const response = await fetch('https://amplacebackend.amfoss.in/api/get_pixel'); 21 | const data = await response.json(); 22 | console.log(data); 23 | if (data.success) { 24 | const updatedPixels = data.pixels.map(pixel => ({ 25 | x: pixel.X, 26 | y: pixel.Y, 27 | hex: pixel['hex-code'], 28 | user: pixel.user, 29 | updatedAt: pixel.updated_at, 30 | })); 31 | setPixelDB(updatedPixels); 32 | console.log(updatedPixels); 33 | } 34 | } catch (error) { 35 | console.error('Error fetching pixel data:', error); 36 | } 37 | }; 38 | 39 | const fetchLeaderboardData = async () => { 40 | try { 41 | const response = await fetch('https://amplacebackend.amfoss.in/api/get_user_details'); 42 | const data = await response.json(); 43 | console.log(data); 44 | if (data.success) { 45 | const sortedLeaderboard = data.user_data.sort((a, b) => b.score - a.score); 46 | setLeaderboardData(sortedLeaderboard); 47 | } 48 | } catch (error) { 49 | console.error('Error fetching leaderboard data:', error); 50 | } 51 | }; 52 | 53 | useEffect(() => { 54 | const mainCanvas = mainCanvasRef.current; 55 | const overlayCanvas = overlayCanvasRef.current; 56 | 57 | if (mainCanvas && overlayCanvas) { 58 | const mainCtx = mainCanvas.getContext('2d'); 59 | const overlayCtx = overlayCanvas.getContext('2d'); 60 | 61 | const drawGrid = (ctx) => { 62 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 63 | 64 | for (let y = 0; y < rows; y++) { 65 | for (let x = 0; x < cols; x++) { 66 | const pixel = pixel_db.find(p => p.x === x && p.y === y); 67 | const fillColor = pixel ? pixel.hex : '#ffffff'; 68 | 69 | ctx.fillStyle = fillColor; 70 | ctx.fillRect(x * boxSize, y * boxSize, boxSize, boxSize); 71 | } 72 | } 73 | 74 | if (highlightedPixel) { 75 | const { x, y } = highlightedPixel; 76 | ctx.strokeStyle = 'black'; 77 | ctx.lineWidth = 1; 78 | ctx.strokeRect(x * boxSize, y * boxSize, boxSize, boxSize); 79 | } 80 | }; 81 | 82 | drawGrid(overlayCtx); 83 | 84 | let isDragging = false; 85 | let startX, startY; 86 | 87 | const handleMouseDown = (event) => { 88 | isDragging = true; 89 | startX = event.clientX; 90 | startY = event.clientY; 91 | }; 92 | 93 | const handleMouseMove = (event) => { 94 | if (isDragging) { 95 | const dx = event.clientX - startX; 96 | const dy = event.clientY - startY; 97 | 98 | overlayCanvas.style.top = `${overlayPositionRef.current.top + dy}px`; 99 | overlayCanvas.style.left = `${overlayPositionRef.current.left + dx}px`; 100 | } 101 | }; 102 | 103 | const handleMouseUp = () => { 104 | if (isDragging) { 105 | isDragging = false; 106 | 107 | const newTop = parseFloat(overlayCanvas.style.top) || 0; 108 | const newLeft = parseFloat(overlayCanvas.style.left) || 0; 109 | 110 | overlayPositionRef.current = { top: newTop, left: newLeft }; 111 | } 112 | }; 113 | 114 | const handleMouseLeave = () => { 115 | isDragging = false; 116 | }; 117 | 118 | const handleClick = (event) => { 119 | const rect = overlayCanvas.getBoundingClientRect(); 120 | const x = Math.floor((event.clientX - rect.left) / boxSize); 121 | const y = Math.floor((event.clientY - rect.top) / boxSize); 122 | 123 | const clickedPixel = pixel_db.find(p => p.x === x && p.y === y); 124 | if (clickedPixel) { 125 | setInfo(`Clicked Box: (${clickedPixel.x}, ${clickedPixel.y}), Color: ${clickedPixel.hex}, User: ${clickedPixel.user}`); 126 | setHighlightedPixel({ x: clickedPixel.x, y: clickedPixel.y }); 127 | } else { 128 | setInfo(`Clicked Box: (${x}, ${y})`); 129 | setHighlightedPixel({ x: x, y: y }); 130 | } 131 | 132 | drawGrid(overlayCtx); 133 | }; 134 | 135 | overlayCanvas.addEventListener('mousedown', handleMouseDown); 136 | overlayCanvas.addEventListener('mousemove', handleMouseMove); 137 | overlayCanvas.addEventListener('mouseup', handleMouseUp); 138 | overlayCanvas.addEventListener('mouseleave', handleMouseLeave); 139 | overlayCanvas.addEventListener('click', handleClick); 140 | 141 | return () => { 142 | overlayCanvas.removeEventListener('mousedown', handleMouseDown); 143 | overlayCanvas.removeEventListener('mousemove', handleMouseMove); 144 | overlayCanvas.removeEventListener('mouseup', handleMouseUp); 145 | overlayCanvas.removeEventListener('mouseleave', handleMouseLeave); 146 | overlayCanvas.removeEventListener('click', handleClick); 147 | }; 148 | } 149 | 150 | calculateLeaderboardData(); 151 | }, [pixel_db, highlightedPixel]); 152 | 153 | useEffect(() => { 154 | fetchPixelData(); 155 | fetchLeaderboardData(); // Fetch leaderboard data here 156 | 157 | const interval = setInterval(() => { 158 | fetchPixelData(); 159 | fetchLeaderboardData(); // Refetch leaderboard data at the same interval 160 | }, 3000); 161 | 162 | return () => clearInterval(interval); 163 | }, []); 164 | 165 | return ( 166 |
167 | 168 | 174 | 185 |
203 | {info} 204 |
205 |
setShowLeaderboard(!showLeaderboard)}> 218 | Leaderboard {showLeaderboard ? '▲' : '▼'} 219 |
220 | {showLeaderboard && ( 221 |
235 |
    236 | {leaderboardData.map(({ user, score }) => ( 237 |
  • 238 | {user}: {score} pixels 239 |
  • 240 | ))} 241 |
242 |
243 | )} 244 |
259 | 260 | GitHub Logo 261 | Contribution Repo 262 | 263 |
264 | 265 | 266 |
267 | ); 268 | }; 269 | 270 | export default Home; 271 | --------------------------------------------------------------------------------