feat: add user and admin page
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
backend/alembic/versions/a12a2ecd70b9_.py
Normal file
28
backend/alembic/versions/a12a2ecd70b9_.py
Normal file
@@ -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
|
||||||
28
backend/alembic/versions/ab09ab5070f7_initial.py
Normal file
28
backend/alembic/versions/ab09ab5070f7_initial.py
Normal file
@@ -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
|
||||||
Binary file not shown.
@@ -5,6 +5,7 @@ from fastapi import HTTPException
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
import schemas
|
import schemas
|
||||||
|
from schemas import ItemResponse
|
||||||
from services import crud
|
from services import crud
|
||||||
|
|
||||||
from app.db.session import SessionLocal
|
from app.db.session import SessionLocal
|
||||||
@@ -14,30 +15,26 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
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
|
# CREATE ITEM
|
||||||
@router.post(
|
@router.post("/items", response_model=ItemResponse)
|
||||||
"/items",
|
|
||||||
response_model=schemas.ItemResponse
|
|
||||||
)
|
|
||||||
def create_item(
|
def create_item(
|
||||||
item: schemas.ItemCreate,
|
item: schemas.ItemCreate,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
return crud.create_item(db, item.title)
|
||||||
return crud.create_item(
|
|
||||||
db,
|
|
||||||
item.title
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# DELETE ITEM
|
# DELETE ITEM
|
||||||
@@ -46,19 +43,7 @@ def delete_item(
|
|||||||
item_id: int,
|
item_id: int,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
item = crud.delete_item(db, item_id)
|
||||||
item = crud.delete_item(
|
|
||||||
db,
|
|
||||||
item_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not item:
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
raise HTTPException(
|
return {"message": "Item deleted"}
|
||||||
status_code=404,
|
|
||||||
detail="Item not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"message": "Item deleted"
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -17,6 +17,5 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app.include_router(admin_router)
|
app.include_router(admin_router, prefix="/admin")
|
||||||
|
app.include_router(user_router, prefix="/user")
|
||||||
app.include_router(user_router)
|
|
||||||
BIN
backend/wheel.db
BIN
backend/wheel.db
Binary file not shown.
20
frontend/admin.html
Normal file
20
frontend/admin.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>پنل ادمین</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>مدیریت آیتمها</h1>
|
||||||
|
|
||||||
|
<div id="add-section">
|
||||||
|
<input type="text" id="new-item" placeholder="نام آیتم جدید">
|
||||||
|
<button id="add-btn">اضافه کن</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="items-list"></ul>
|
||||||
|
|
||||||
|
<script type="module" src="assets/js/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
frontend/assets/css/style.css
Normal file
61
frontend/assets/css/style.css
Normal file
@@ -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;
|
||||||
33
frontend/assets/js/admin.js
Normal file
33
frontend/assets/js/admin.js
Normal file
@@ -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 = `
|
||||||
|
<span>${item.title}</span>
|
||||||
|
<button onclick="removeItem(${item.id})">حذف</button>
|
||||||
|
`;
|
||||||
|
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();
|
||||||
20
frontend/assets/js/api.js
Normal file
20
frontend/assets/js/api.js
Normal file
@@ -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());
|
||||||
82
frontend/assets/js/wheel.js
Normal file
82
frontend/assets/js/wheel.js
Normal file
@@ -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();
|
||||||
17
frontend/index.html
Normal file
17
frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>گردونه شانس</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>گردونه شانس</h1>
|
||||||
|
<canvas id="wheel" width="400" height="400"></canvas>
|
||||||
|
<br>
|
||||||
|
<button id="spin-btn">بچرخون!</button>
|
||||||
|
<p id="result"></p>
|
||||||
|
|
||||||
|
<script type="module" src="assets/js/wheel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user