diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 02039a9..5524bfe 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/alembic/versions/__pycache__/a12a2ecd70b9_.cpython-313.pyc b/backend/alembic/versions/__pycache__/a12a2ecd70b9_.cpython-313.pyc new file mode 100644 index 0000000..07d47b2 Binary files /dev/null and b/backend/alembic/versions/__pycache__/a12a2ecd70b9_.cpython-313.pyc differ diff --git a/backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc b/backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc new file mode 100644 index 0000000..94d43be Binary files /dev/null and b/backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc differ diff --git a/backend/alembic/versions/a12a2ecd70b9_.py b/backend/alembic/versions/a12a2ecd70b9_.py new file mode 100644 index 0000000..5ec8cda --- /dev/null +++ b/backend/alembic/versions/a12a2ecd70b9_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: a12a2ecd70b9 +Revises: 96dff3d4cf1c +Create Date: 2026-05-17 09:46:42.181807 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a12a2ecd70b9' +down_revision: Union[str, Sequence[str], None] = '96dff3d4cf1c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/backend/alembic/versions/ab09ab5070f7_initial.py b/backend/alembic/versions/ab09ab5070f7_initial.py new file mode 100644 index 0000000..e6efba1 --- /dev/null +++ b/backend/alembic/versions/ab09ab5070f7_initial.py @@ -0,0 +1,28 @@ +"""initial + +Revision ID: ab09ab5070f7 +Revises: a12a2ecd70b9 +Create Date: 2026-05-17 11:44:50.049279 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ab09ab5070f7' +down_revision: Union[str, Sequence[str], None] = 'a12a2ecd70b9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/backend/api/__pycache__/admin.cpython-313.pyc b/backend/api/__pycache__/admin.cpython-313.pyc index 7fd52b4..e10c474 100644 Binary files a/backend/api/__pycache__/admin.cpython-313.pyc and b/backend/api/__pycache__/admin.cpython-313.pyc differ diff --git a/backend/api/admin.py b/backend/api/admin.py index 79d823f..afc31ed 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -5,6 +5,7 @@ from fastapi import HTTPException from sqlalchemy.orm import Session import schemas +from schemas import ItemResponse from services import crud from app.db.session import SessionLocal @@ -14,30 +15,26 @@ router = APIRouter() def get_db(): - db = SessionLocal() - try: yield db - finally: db.close() +# GET ALL ITEMS +@router.get("/items", response_model=list[ItemResponse]) +def get_items(db: Session = Depends(get_db)): + return crud.get_items(db) + + # CREATE ITEM -@router.post( - "/items", - response_model=schemas.ItemResponse -) +@router.post("/items", response_model=ItemResponse) def create_item( item: schemas.ItemCreate, db: Session = Depends(get_db) ): - - return crud.create_item( - db, - item.title - ) + return crud.create_item(db, item.title) # DELETE ITEM @@ -46,19 +43,7 @@ def delete_item( item_id: int, db: Session = Depends(get_db) ): - - item = crud.delete_item( - db, - item_id - ) - + item = crud.delete_item(db, item_id) if not item: - - raise HTTPException( - status_code=404, - detail="Item not found" - ) - - return { - "message": "Item deleted" - } \ No newline at end of file + raise HTTPException(status_code=404, detail="Item not found") + return {"message": "Item deleted"} \ No newline at end of file diff --git a/backend/app/db/__pycache__/session.cpython-313.pyc b/backend/app/db/__pycache__/session.cpython-313.pyc index 20b6f71..1267946 100644 Binary files a/backend/app/db/__pycache__/session.cpython-313.pyc and b/backend/app/db/__pycache__/session.cpython-313.pyc differ diff --git a/backend/main.py b/backend/main.py index f394234..003c12e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,6 +17,5 @@ app.add_middleware( ) -app.include_router(admin_router) - -app.include_router(user_router) \ No newline at end of file +app.include_router(admin_router, prefix="/admin") +app.include_router(user_router, prefix="/user") \ No newline at end of file diff --git a/backend/wheel.db b/backend/wheel.db index 89e4764..36027c6 100644 Binary files a/backend/wheel.db and b/backend/wheel.db differ diff --git a/frontend-admin/admin.html b/frontend-admin/admin.html deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-admin/app.js b/frontend-admin/app.js deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-admin/index.html b/frontend-admin/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/admin.html b/frontend/admin.html new file mode 100644 index 0000000..0ae1a68 --- /dev/null +++ b/frontend/admin.html @@ -0,0 +1,20 @@ + + + + + پنل ادمین + + + +

مدیریت آیتم‌ها

+ +
+ + +
+ + + + + + \ No newline at end of file diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css new file mode 100644 index 0000000..b183966 --- /dev/null +++ b/frontend/assets/css/style.css @@ -0,0 +1,61 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: Tahoma, sans-serif; + background: #f5f5f5; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 20px; + gap: 20px; +} + +h1 { + color: #2c3e50; + margin-bottom: 10px; +} + +canvas { + border-radius: 50%; + box-shadow: 0 4px 20px rgba(0,0,0,0.2); +} + +button { + padding: 12px 30px; + font-size: 18px; + font-family: Tahoma, sans-serif; + background: #e74c3c; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} + +button:hover { + background: #c0392b; +} + +button:disabled { + background: #aaa; + cursor: not-allowed; +} + +#result { + font-size: 24px; + font-weight: bold; + color: #2c3e50; +} + +/* ادمین */ +#add-section { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +input { + padding: 10px; \ No newline at end of file diff --git a/frontend/assets/js/admin.js b/frontend/assets/js/admin.js new file mode 100644 index 0000000..2058645 --- /dev/null +++ b/frontend/assets/js/admin.js @@ -0,0 +1,33 @@ +import { adminGetItems, createItem, deleteItem } from './api.js'; + +const list = document.getElementById('items-list'); +const input = document.getElementById('new-item'); +const addBtn = document.getElementById('add-btn'); + +async function renderItems() { + const items = await adminGetItems(); + list.innerHTML = ''; + items.forEach(item => { + const li = document.createElement('li'); + li.innerHTML = ` + ${item.title} + + `; + list.appendChild(li); + }); +} + +window.removeItem = async (id) => { + await deleteItem(id); + renderItems(); +}; + +addBtn.onclick = async () => { + const title = input.value.trim(); + if (!title) return; + await createItem(title); + input.value = ''; + renderItems(); +}; + +renderItems(); \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js new file mode 100644 index 0000000..a55795f --- /dev/null +++ b/frontend/assets/js/api.js @@ -0,0 +1,20 @@ +const BASE = 'http://127.0.0.1:8000'; + +export const getItems = () => + fetch(`${BASE}/user/items`).then(r => r.json()); + +export const spinWheel = () => + fetch(`${BASE}/user/spin`, { method: 'POST' }).then(r => r.json()); + +export const adminGetItems = () => + fetch(`${BASE}/admin/items`).then(r => r.json()); + +export const createItem = (title) => + fetch(`${BASE}/admin/items`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }) + }).then(r => r.json()); + +export const deleteItem = (id) => + fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(r => r.json()); \ No newline at end of file diff --git a/frontend/assets/js/wheel.js b/frontend/assets/js/wheel.js new file mode 100644 index 0000000..83cc99b --- /dev/null +++ b/frontend/assets/js/wheel.js @@ -0,0 +1,82 @@ +import { getItems, spinWheel } from './api.js'; + +const canvas = document.getElementById('wheel'); +const ctx = canvas.getContext('2d'); +const btn = document.getElementById('spin-btn'); +const result = document.getElementById('result'); +const colors = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c']; + +let items = []; +let angle = 0; +let spinning = false; + +async function init() { + items = await getItems(); + drawWheel(angle); +} + +function drawWheel(rotation) { + if (items.length === 0) return; + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const r = cx - 10; + const slice = (2 * Math.PI) / items.length; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + items.forEach((item, i) => { + const start = rotation + i * slice; + const end = start + slice; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r, start, end); + ctx.fillStyle = colors[i % colors.length]; + ctx.fill(); + ctx.stroke(); + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(start + slice / 2); + ctx.textAlign = 'right'; + ctx.fillStyle = '#fff'; + ctx.font = 'bold 16px Tahoma'; + ctx.fillText(item.title, r - 10, 5); + ctx.restore(); + }); +} + +btn.onclick = async () => { + if (spinning || items.length === 0) return; + spinning = true; + btn.disabled = true; + result.textContent = ''; + + const data = await spinWheel(); + const winner = data.winner; + const winnerIdx = items.findIndex(i => i.id === winner.id); + const slice = (2 * Math.PI) / items.length; + const targetAngle = angle + (Math.PI * 2 * 5) + + (Math.PI * 2 - (winnerIdx * slice + slice / 2)); + + const duration = 4000; + const start = performance.now(); + const from = angle; + + function animate(now) { + const elapsed = now - start; + const t = Math.min(elapsed / duration, 1); + const ease = 1 - Math.pow(1 - t, 3); + angle = from + (targetAngle - from) * ease; + drawWheel(angle); + + if (t < 1) { + requestAnimationFrame(animate); + } else { + spinning = false; + btn.disabled = false; + result.textContent = `🎉 ${winner.title}`; + } + } + + requestAnimationFrame(animate); +}; + +init(); \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0713603 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + گردونه شانس + + + +

گردونه شانس

+ +
+ +

+ + + + \ No newline at end of file