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 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"
}

View File

@@ -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)

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>