feat: add user and admin page

This commit is contained in:
2026-05-24 02:27:31 +03:30
parent be6e1fab8e
commit 2d99f0554d
19 changed files with 303 additions and 30 deletions

View 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

View 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

View File

@@ -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"
}
raise HTTPException(status_code=404, detail="Item not found")
return {"message": "Item deleted"}

View File

@@ -17,6 +17,5 @@ app.add_middleware(
)
app.include_router(admin_router)
app.include_router(user_router)
app.include_router(admin_router, prefix="/admin")
app.include_router(user_router, prefix="/user")

Binary file not shown.

View File

20
frontend/admin.html Normal file
View 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>

View 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;

View 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
View 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());

View 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
View 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>