├── 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 |
--------------------------------------------------------------------------------
/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 | 
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 |
264 |
265 |
266 |
267 | );
268 | };
269 |
270 | export default Home;
271 |
--------------------------------------------------------------------------------