git add .

git commit -m "feat: improve wheel UI with gold pointer and center circle sync"
This commit is contained in:
2026-05-30 13:29:48 +03:30
parent 1618580439
commit ed82bf7f1f
27 changed files with 137 additions and 1155 deletions

View File

@@ -1,3 +1,4 @@
DATABASE_URL=sqlite:///./spinwheel.db DATABASE_URL=sqlite:///./wheel.db
LOG_ENABLED=true LOG_ENABLED=true
LOG_LEVEL=DEBUG LOG_LEVEL=DEBUG

View File

@@ -1,28 +0,0 @@
"""add prize column
Revision ID: b55808a86947
Revises: ab09ab5070f7
Create Date: 2026-05-29 19:32:41.615653
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b55808a86947'
down_revision: Union[str, Sequence[str], None] = 'ab09ab5070f7'
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

@@ -30,7 +30,7 @@ def get_items(db: Session = Depends(get_db)):
logger.info(f"Items fetched: {len(items)} items") logger.info(f"Items fetched: {len(items)} items")
return items return items
except Exception as e: except Exception as e:
logger.critical(f"Unexpected error fetching items: {e}") logger.error(f"Error fetching items: {e}")
raise raise
@@ -41,11 +41,11 @@ def create_item(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
try: try:
result = crud.create_item(db, item.title, item.value) # ✅ value اضافه شد result = crud.create_item(db, item.title)
logger.info(f"Item created: {item.title}") logger.info(f"Item created: {item.title}")
return result return result
except Exception as e: except Exception as e:
logger.critical(f"Unexpected error creating item: {e}") logger.error(f"Error creating item: {e}")
raise raise
@@ -65,29 +65,5 @@ def delete_item(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.critical(f"Unexpected error deleting item: {e}") logger.error(f"Error deleting item: {e}")
raise raise
@router.put("/items/{item_id}", response_model=ItemResponse)
def update_item(
item_id: int,
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
try:
updated = crud.update_item(db, item_id, item.title, item.value)
if not updated:
logger.warning(f"Item not found: {item_id}")
raise HTTPException(status_code=404, detail="Item not found")
logger.info(f"Item updated: {item_id}")
return updated
except HTTPException:
raise
except Exception as e:
logger.critical(f"Unexpected error updating item: {e}")
raise

View File

@@ -29,7 +29,7 @@ def get_items(db: Session = Depends(get_db)):
logger.info(f"User fetched items: {len(items)} items") logger.info(f"User fetched items: {len(items)} items")
return items return items
except Exception as e: except Exception as e:
logger.critical(f"Unexpected error fetching items: {e}") logger.error(f"Error fetching items: {e}")
raise raise
@@ -38,26 +38,4 @@ def get_items(db: Session = Depends(get_db)):
def spin(db: Session = Depends(get_db)): def spin(db: Session = Depends(get_db)):
try: try:
winner = crud.spin_wheel(db) winner = crud.spin_wheel(db)
if not winner:
if not winner:
logger.warning("Spin attempted but no items available")
raise HTTPException(
status_code=400,
detail="No items available"
)
logger.info(f"Spin result: {winner.title}")
return {
"winner": {
"id": winner.id,
"title": winner.title
}
}
except HTTPException:
raise
except Exception as e:
logger.critical(f"Unexpected error during spin: {e}")
raise

View File

@@ -18,9 +18,4 @@ class Item(Base):
title = Column( title = Column(
String, String,
nullable=False nullable=False
)
value = Column(
Integer,
nullable=False
) )

View File

@@ -1,16 +1,10 @@
from pydantic import Field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):
DATABASE_URL: str = Field(..., alias="DATABASE_URL")
LOG_ENABLED: bool = Field(True, alias="LOG_ENABLED") DATABASE_URL: str
LOG_LEVEL: str = Field("INFO", alias="LOG_LEVEL")
class Config: class Config:
env_file = ".env" env_file = ".env"
populate_by_name = True
settings = Settings() settings = Settings()

View File

@@ -1,206 +1 @@
2026-05-24 09:52:10 | INFO | test | test 2026-05-24 09:52:10 | INFO | test | test
2026-05-24 12:00:16 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:43 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:47 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:57 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:59 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:01:05 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:42 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:49 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:51 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:12:18 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:24:46 | INFO | admin | Items fetched: 0 items
2026-05-24 12:24:55 | INFO | admin | Item created: جایزه 1
2026-05-24 12:24:55 | INFO | admin | Items fetched: 1 items
2026-05-24 12:25:07 | INFO | admin | Item created: جایزه 2
2026-05-24 12:25:07 | INFO | admin | Items fetched: 2 items
2026-05-24 12:25:12 | INFO | admin | Item created: جایزه 3
2026-05-24 12:25:12 | INFO | admin | Items fetched: 3 items
2026-05-24 12:25:17 | INFO | user | User fetched items: 3 items
2026-05-24 12:25:19 | INFO | user | Spin result: جایزه 3
2026-05-24 12:26:33 | INFO | admin | Item created: پوچ
2026-05-24 12:26:33 | INFO | admin | Items fetched: 4 items
2026-05-24 12:26:38 | INFO | user | User fetched items: 4 items
2026-05-24 12:26:40 | INFO | user | Spin result: جایزه 1
2026-05-24 12:41:01 | INFO | user | User fetched items: 4 items
2026-05-24 12:41:02 | INFO | user | User fetched items: 4 items
2026-05-24 12:41:04 | INFO | user | Spin result: پوچ
2026-05-24 12:45:29 | INFO | user | Spin result: پوچ
2026-05-24 12:49:58 | INFO | user | Spin result: جایزه 1
2026-05-24 12:50:31 | INFO | user | Spin result: پوچ
2026-05-24 12:50:59 | INFO | user | Spin result: جایزه 2
2026-05-24 12:51:10 | INFO | user | Spin result: جایزه 3
2026-05-24 12:57:40 | INFO | admin | Item deleted: 4
2026-05-24 12:57:40 | INFO | admin | Items fetched: 3 items
2026-05-24 12:57:48 | INFO | admin | Item created: پوچ
2026-05-24 12:57:48 | INFO | admin | Items fetched: 4 items
2026-05-24 12:58:01 | INFO | admin | Item created: جایزه 3
2026-05-24 12:58:01 | INFO | admin | Items fetched: 5 items
2026-05-24 12:58:49 | INFO | user | User fetched items: 5 items
2026-05-29 19:43:29 | INFO | admin | Items fetched: 5 items
2026-05-29 19:43:34 | INFO | admin | Items fetched: 5 items
2026-05-29 19:43:35 | INFO | admin | Items fetched: 5 items
2026-05-29 19:44:03 | INFO | admin | Items fetched: 5 items
2026-05-29 19:44:05 | INFO | admin | Items fetched: 5 items
2026-05-29 19:44:28 | INFO | user | User fetched items: 5 items
2026-05-29 19:45:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:45:18 | INFO | admin | Items fetched: 5 items
2026-05-29 19:45:19 | INFO | admin | Items fetched: 5 items
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items
2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items
2026-05-29 19:47:15 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:18 | INFO | admin | Items fetched: 5 items
2026-05-29 19:47:20 | INFO | user | User fetched items: 5 items
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
2026-05-29 19:48:16 | INFO | admin | Items fetched: 5 items
2026-05-29 19:49:38 | INFO | user | User fetched items: 5 items
2026-05-29 19:49:45 | INFO | admin | Items fetched: 5 items
2026-05-29 19:53:58 | INFO | admin | Items fetched: 5 items
2026-05-29 19:53:59 | INFO | admin | Items fetched: 5 items
2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items
2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items
2026-05-29 20:00:58 | INFO | admin | Items fetched: 0 items
2026-05-29 20:01:00 | INFO | admin | Items fetched: 0 items
2026-05-29 20:01:03 | INFO | admin | Items fetched: 0 items
2026-05-29 20:02:53 | INFO | admin | Items fetched: 0 items
2026-05-29 20:03:50 | INFO | admin | Items fetched: 0 items
2026-05-29 20:03:51 | INFO | admin | Items fetched: 0 items
2026-05-29 20:03:53 | INFO | admin | Items fetched: 0 items
2026-05-29 20:05:26 | INFO | admin | Items fetched: 0 items
2026-05-29 20:11:18 | INFO | admin | Items fetched: 0 items
2026-05-29 20:11:59 | INFO | admin | Items fetched: 0 items
2026-05-29 20:12:12 | INFO | admin | Items fetched: 0 items
2026-05-29 20:15:16 | INFO | admin | Items fetched: 0 items
2026-05-29 20:15:27 | INFO | admin | Items fetched: 0 items
2026-05-29 20:20:21 | INFO | admin | Items fetched: 0 items
2026-05-29 20:29:30 | INFO | admin | Items fetched: 0 items
2026-05-29 20:29:33 | INFO | admin | Items fetched: 0 items
2026-05-29 20:29:40 | INFO | admin | Item created: سفر
2026-05-29 20:29:40 | INFO | admin | Items fetched: 1 items
2026-05-29 20:48:28 | INFO | admin | Items fetched: 1 items
2026-05-29 20:48:31 | INFO | admin | Items fetched: 1 items
2026-05-29 20:48:45 | INFO | admin | Item created: طلا
2026-05-29 20:48:45 | INFO | admin | Items fetched: 2 items
2026-05-29 20:48:52 | INFO | user | User fetched items: 2 items
2026-05-29 20:48:55 | INFO | user | Spin result: سفر
2026-05-29 20:49:35 | INFO | admin | Item deleted: 1
2026-05-29 20:49:35 | INFO | admin | Items fetched: 1 items
2026-05-29 20:50:46 | INFO | admin | Items fetched: 1 items
2026-05-29 20:51:12 | INFO | admin | Item created: خانه
2026-05-29 20:51:12 | INFO | admin | Items fetched: 2 items
2026-05-29 20:51:18 | INFO | user | User fetched items: 2 items
2026-05-29 20:51:23 | INFO | user | User fetched items: 2 items
2026-05-29 20:51:26 | INFO | user | User fetched items: 2 items
2026-05-29 20:51:40 | INFO | admin | Item created: زمين
2026-05-29 20:51:40 | INFO | admin | Items fetched: 3 items
2026-05-29 20:51:50 | INFO | user | User fetched items: 3 items
2026-05-29 20:51:52 | INFO | user | Spin result: طلا
2026-05-29 20:52:07 | INFO | user | Spin result: زمين
2026-05-29 20:52:13 | INFO | user | Spin result: طلا
2026-05-29 21:00:39 | INFO | user | User fetched items: 3 items
2026-05-29 21:00:41 | INFO | user | Spin result: زمين
2026-05-29 21:02:05 | INFO | user | User fetched items: 3 items
2026-05-29 21:02:09 | INFO | user | Spin result: طلا
2026-05-29 21:02:20 | INFO | user | Spin result: خانه
2026-05-29 21:04:48 | INFO | user | User fetched items: 3 items
2026-05-29 21:04:56 | INFO | user | Spin result: خانه
2026-05-29 21:05:32 | INFO | user | User fetched items: 3 items
2026-05-29 21:06:04 | INFO | user | Spin result: زمين
2026-05-29 21:20:30 | INFO | admin | Item deleted: 2
2026-05-29 21:20:30 | INFO | admin | Items fetched: 2 items
2026-05-29 21:20:33 | INFO | admin | Item deleted: 3
2026-05-29 21:20:33 | INFO | admin | Items fetched: 1 items
2026-05-29 21:20:34 | INFO | admin | Item deleted: 4
2026-05-29 21:20:34 | INFO | admin | Items fetched: 0 items
2026-05-29 21:21:07 | INFO | admin | Item created: پوچ
2026-05-29 21:21:07 | INFO | admin | Items fetched: 1 items
2026-05-29 21:21:18 | INFO | admin | Item created: تخفيف يک ماهه
2026-05-29 21:21:18 | INFO | admin | Items fetched: 2 items
2026-05-29 21:21:47 | INFO | admin | Item created: تخفيف يک ساله
2026-05-29 21:21:47 | INFO | admin | Items fetched: 3 items
2026-05-29 21:22:08 | INFO | admin | Item created: تخفيف سه ماهه
2026-05-29 21:22:08 | INFO | admin | Items fetched: 4 items
2026-05-29 21:22:36 | INFO | admin | Item created: 10% تخفيف يک ماهه
2026-05-29 21:22:36 | INFO | admin | Items fetched: 5 items
2026-05-29 21:22:44 | INFO | admin | Item created: پوچ
2026-05-29 21:22:44 | INFO | admin | Items fetched: 6 items
2026-05-29 21:22:50 | INFO | user | User fetched items: 6 items
2026-05-29 21:22:53 | INFO | user | Spin result: تخفيف سه ماهه
2026-05-29 21:28:31 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-29 21:30:11 | INFO | user | Spin result: 10% تخفيف يک ماهه
2026-05-29 23:55:52 | INFO | admin | Items fetched: 6 items
2026-05-29 23:56:28 | INFO | admin | Items fetched: 6 items
2026-05-29 23:56:41 | INFO | admin | Items fetched: 6 items
2026-05-30 00:02:00 | INFO | admin | Items fetched: 6 items
2026-05-30 00:02:05 | INFO | admin | Item updated: 1
2026-05-30 00:02:05 | INFO | admin | Items fetched: 6 items
2026-05-30 00:02:13 | INFO | admin | Item updated: 1
2026-05-30 00:02:13 | INFO | admin | Items fetched: 6 items
2026-05-30 00:02:19 | INFO | admin | Item updated: 1
2026-05-30 00:02:19 | INFO | admin | Items fetched: 6 items
2026-05-30 00:02:36 | INFO | user | Spin result: پوچ
2026-05-30 00:05:46 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-30 00:13:50 | INFO | user | User fetched items: 6 items
2026-05-30 00:14:24 | INFO | user | User fetched items: 6 items
2026-05-30 00:31:05 | INFO | user | User fetched items: 6 items
2026-05-30 00:33:44 | INFO | user | Spin result: تخفيف يک ساله
2026-05-30 00:43:06 | INFO | user | User fetched items: 6 items
2026-05-30 00:43:10 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-30 00:45:02 | INFO | user | User fetched items: 6 items
2026-05-30 00:45:04 | INFO | user | Spin result: 10% تخفيف يک ماهه
2026-05-30 00:45:41 | INFO | user | Spin result: تخفيف يک ساله
2026-05-30 00:46:22 | INFO | admin | Item created: تخفیف هفتگی
2026-05-30 00:46:22 | INFO | admin | Items fetched: 7 items
2026-05-30 00:46:30 | INFO | user | User fetched items: 7 items
2026-05-30 00:47:47 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-30 00:51:23 | INFO | user | User fetched items: 7 items
2026-05-30 00:51:28 | INFO | user | Spin result: تخفيف سه ماهه
2026-05-30 00:58:22 | INFO | user | User fetched items: 7 items
2026-05-30 01:02:03 | INFO | user | User fetched items: 7 items
2026-05-30 01:04:14 | INFO | admin | Item created: پوچ
2026-05-30 01:04:14 | INFO | admin | Items fetched: 8 items
2026-05-30 01:05:21 | INFO | user | User fetched items: 8 items
2026-05-30 01:12:18 | INFO | user | Spin result: پوچ
2026-05-30 01:12:25 | INFO | user | Spin result: تخفيف سه ماهه

View File

@@ -1,40 +0,0 @@
2026-05-24 12:00:16 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:43 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:47 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:57 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:00:59 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:01:05 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:42 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
[SQL: SELECT items.id AS items_id, items.title AS items_title
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:49 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:09:51 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-05-24 12:12:18 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
[SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)

View File

@@ -1,29 +1,21 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.db.session import engine
from app.db.session import Base
from app.db.models import Item
app = FastAPI()
from api.admin import router as admin_router from api.admin import router as admin_router
from api.user import router as user_router from api.user import router as user_router
app.include_router(user_router, prefix="/user")
Base.metadata.create_all(bind=engine) app = FastAPI()
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[ allow_origins=["*"],
"http://localhost:3000",
"http://127.0.0.1:3000",
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
app.include_router(admin_router, prefix="/admin")
app.include_router(admin_router, prefix="/admin")
app.include_router(user_router, prefix="/user")

View File

@@ -1,20 +1,11 @@
from pydantic import BaseModel from pydantic import BaseModel
class ItemCreate(BaseModel): class ItemCreate(BaseModel):
title: str title: str
value: int
class ItemUpdate(BaseModel):
title: str
value: int
class ItemResponse(BaseModel): class ItemResponse(BaseModel):
id: int id: int
title: str title: str
value: int
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,35 +1,61 @@
import random import random
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.db.models import Item from app.db.models import Item
def get_items(db: Session):
def get_items(
db: Session
):
return db.query(Item).all() return db.query(Item).all()
def create_item(db: Session, title: str, value: int): # ✅ prize → value
item = Item(title=title, value=value) # ✅ prize → value def create_item(
db: Session,
title: str
):
item = Item(
title=title
)
db.add(item) db.add(item)
db.commit() db.commit()
db.refresh(item) db.refresh(item)
return item return item
def update_item(db: Session, item_id: int, title: str, value: int): # ✅ prize → value
item = db.query(Item).filter(Item.id == item_id).first()
if item:
item.title = title
item.value = value # ✅ prize → value
db.commit()
db.refresh(item)
return item
def delete_item(db: Session, item_id: int): def delete_item(
item = db.query(Item).filter(Item.id == item_id).first() db: Session,
item_id: int
):
item = db.query(Item).filter(
Item.id == item_id
).first()
if item: if item:
db.delete(item) db.delete(item)
db.commit() db.commit()
return item return item
def spin_wheel(db: Session):
def spin_wheel(
db: Session
):
items = db.query(Item).all() items = db.query(Item).all()
if not items: if not items:
return None return None
return random.choice(items) return random.choice(items)

View File

@@ -1,92 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>پنل ادمین</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>پنل مدیریت</title>
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body> <body>
<h1>مدیریت آیتم‌ها</h1>
<div class="admin-container"> <div id="add-section">
<input type="text" id="new-item" placeholder="نام آیتم جدید">
<div class="admin-header"> <button id="add-btn">اضافه کن</button>
<h1> مدیریت گردونه شانس</h1>
<p>مدیریت آیتم‌ها و جوایز گردونه</p>
</div>
<!-- افزودن آیتم -->
<div class="add-section">
<input
type="text"
id="title-input"
placeholder="عنوان جایزه"
>
<input
type="number"
id="value-input"
placeholder="مقدار جایزه"
>
<button id="add-btn">
افزودن
</button>
</div>
<!-- جدول -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<th>عنوان جایزه</th>
<th>مقدار</th>
<th>عملیات</th>
</tr>
</thead>
<tbody id="items-table-body">
<!-- ردیف نمونه (دینامیک توسط JS ساخته می‌شود) -->
<!--
<tr>
<td>1</td>
<td>جایزه تست</td>
<td>100</td>
<td class="actions">
<button class="edit-btn" data-id="1">
✏️ ویرایش
</button>
<button class="delete-btn" data-id="1">
🗑 حذف
</button>
</td>
</tr>
-->
</tbody>
</table>
</div>
</div> </div>
<ul id="items-list"></ul>
<script type="module" src="assets/js/admin.js"></script> <script type="module" src="assets/js/admin.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,325 +1,61 @@
/* =========================
ADMIN PANEL
========================= */
.admin-container {
width: 100%;
max-width: 1200px;
padding: 30px;
}
.admin-header {
text-align: center;
margin-bottom: 30px;
}
.admin-header h1 {
color: #FFD700;
font-size: 38px;
text-shadow: 0 0 15px rgba(255,215,0,0.5);
}
.admin-header p {
color: #f5d6a0;
margin-top: 10px;
font-size: 18px;
}
.add-section {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: 20px;
display: flex;
gap: 15px;
margin-bottom: 30px;
box-shadow:
0 10px 30px rgba(0,0,0,0.4),
inset 0 0 20px rgba(255,255,255,0.03);
}
.add-section input {
flex: 1;
padding: 14px;
border: none;
border-radius: 12px;
background: rgba(255,255,255,0.08);
color: white;
font-size: 16px;
outline: none;
}
.add-section input::placeholder {
color: rgba(255,255,255,0.5);
}
#add-btn {
padding: 14px 28px;
background: linear-gradient(
135deg,
#FFD700,
#D4AC0D
);
color: #4a0f0f;
border: none;
border-radius: 14px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
box-shadow:
0 4px 20px rgba(255,215,0,0.3);
}
#add-btn:hover {
transform: translateY(-2px);
box-shadow:
0 8px 25px rgba(255,215,0,0.45);
}
.table-wrapper {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(10px);
border-radius: 22px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
box-shadow:
0 10px 40px rgba(0,0,0,0.45);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background:
linear-gradient(
135deg,
#FFD700,
#D4AC0D
);
color: #4a0f0f;
}
th {
padding: 18px;
font-size: 17px;
}
td {
padding: 18px;
text-align: center;
color: white;
}
tbody tr {
border-bottom:
1px solid rgba(255,255,255,0.06);
transition: 0.2s;
}
tbody tr:hover {
background:
rgba(255,255,255,0.05);
}
.action-buttons {
display: flex;
justify-content: center;
gap: 10px;
}
.edit-btn,
.delete-btn {
border: none;
padding: 10px 16px;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
transition: 0.2s;
}
.edit-btn {
background:
linear-gradient(
135deg,
#9B59B6,
#7D3C98
);
color: white;
}
.delete-btn {
background:
linear-gradient(
135deg,
#E74C3C,
#922B21
);
color: white;
}
.edit-btn:hover,
.delete-btn:hover {
transform: scale(1.05);
}
@media (max-width: 768px) {
.add-section {
flex-direction: column;
}
table {
font-size: 14px;
}
th,
td {
padding: 12px;
}
}
/* =========================
WHEEL PAGE
========================= */
* { * {
box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box;
} }
body { body {
min-height: 100vh;
background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: Tahoma, sans-serif; font-family: Tahoma, sans-serif;
} background: #f5f5f5;
.page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 24px; padding: 40px 20px;
padding: 30px; gap: 20px;
} }
.page h1 { h1 {
color: #FFD700; color: #2c3e50;
font-size: 36px; margin-bottom: 10px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
letter-spacing: 2px;
} }
.wheel-wrapper { canvas {
position: relative; border-radius: 50%;
width: 500px; box-shadow: 0 4px 20px rgba(0,0,0,0.2);
height: 500px;
} }
.wheel-wrapper canvas { button {
display: block; padding: 12px 30px;
width: 100%; font-size: 18px;
height: 100%;
}
#spin-btn {
padding: 16px 48px;
font-size: 20px;
font-weight: bold;
font-family: Tahoma, sans-serif; font-family: Tahoma, sans-serif;
background: linear-gradient(135deg, #FFD700, #D4AC0D); background: #e74c3c;
color: #4a0f0f; color: white;
border: none; border: none;
border-radius: 50px; border-radius: 8px;
cursor: pointer; cursor: pointer;
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4);
transition: 0.2s;
} }
#spin-btn:hover { button:hover {
transform: translateY(-3px); background: #c0392b;
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55);
} }
#spin-btn:disabled { button:disabled {
opacity: 0.5; background: #aaa;
cursor: not-allowed; cursor: not-allowed;
transform: none;
} }
#result { #result {
min-height: 50px; font-size: 24px;
text-align: center;
}
.winner-text {
font-size: 28px;
font-weight: bold; font-weight: bold;
color: #FFD700; color: #2c3e50;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.7);
animation: pop 0.4s ease;
} }
@keyframes pop { /* ادمین */
0% { transform: scale(0.5); opacity: 0; } #add-section {
70% { transform: scale(1.1); } display: flex;
100% { transform: scale(1); opacity: 1; } gap: 10px;
margin-bottom: 20px;
} }
@media (max-width: 550px) { input {
.wheel-wrapper { padding: 10px;
width: 340px;
height: 340px;
}
.page h1 {
font-size: 26px;
}
#spin-btn {
font-size: 16px;
padding: 12px 36px;
}
}

View File

@@ -1,103 +1,33 @@
import { import { adminGetItems, createItem, deleteItem } from './api.js';
adminGetItems,
createItem,
deleteItem,
updateItem // 👈 باید اینو هم به api.js اضافه کنی
} from './api.js';
const tableBody = document.getElementById('items-table-body'); const list = document.getElementById('items-list');
const titleInput = document.getElementById('title-input'); const input = document.getElementById('new-item');
const valueInput = document.getElementById('value-input');
const addBtn = document.getElementById('add-btn'); const addBtn = document.getElementById('add-btn');
let editId = null; // 👈 حالت ویرایش
// -------------------- Render --------------------
async function renderItems() { async function renderItems() {
const items = await adminGetItems(); const items = await adminGetItems();
list.innerHTML = '';
tableBody.innerHTML = ''; items.forEach(item => {
const li = document.createElement('li');
items.forEach((item, index) => { li.innerHTML = `
const row = document.createElement('tr'); <span>${item.title}</span>
<button onclick="removeItem(${item.id})">حذف</button>
row.innerHTML = `
<td>${index + 1}</td>
<td>${item.title}</td>
<td>${item.value}</td>
<td class="actions">
<button
class="edit-btn"
onclick="editItem(${item.id}, '${item.title}', ${item.value})"
>
✏️ ویرایش
</button>
<button
class="delete-btn"
onclick="removeItem(${item.id})"
>
🗑 حذف
</button>
</td>
`; `;
list.appendChild(li);
tableBody.appendChild(row);
}); });
} }
// -------------------- Delete --------------------
window.removeItem = async (id) => { window.removeItem = async (id) => {
await deleteItem(id); await deleteItem(id);
renderItems(); renderItems();
}; };
// -------------------- Edit --------------------
window.editItem = (id, title, value) => {
titleInput.value = title;
valueInput.value = value;
editId = id;
addBtn.textContent = 'آپدیت ✏️';
};
// -------------------- Add / Update --------------------
addBtn.onclick = async () => { addBtn.onclick = async () => {
const title = titleInput.value.trim(); const title = input.value.trim();
const value = valueInput.value.trim(); if (!title) return;
await createItem(title);
if (!title || !value) { input.value = '';
alert('همه فیلدها را پر کن');
return;
}
// حالت ویرایش
if (editId) {
await updateItem(editId, {
title,
value: parseInt(value)
});
editId = null;
addBtn.textContent = 'افزودن';
}
// حالت افزودن
else {
await createItem({
title,
value: parseInt(value)
});
}
titleInput.value = '';
valueInput.value = '';
renderItems(); renderItems();
}; };
// -------------------- Init --------------------
renderItems(); renderItems();

View File

@@ -1,40 +1,20 @@
const BASE = 'http://127.0.0.1:8000'; const BASE = 'http://127.0.0.1:8000';
export const getItems = () => export const getItems = () =>
fetch(`${BASE}/user/items`) fetch(`${BASE}/user/items`).then(r => r.json());
.then(r => r.json());
export const spinWheel = () => export const spinWheel = () =>
fetch(`${BASE}/user/spin`, { fetch(`${BASE}/user/spin`, { method: 'POST' }).then(r => r.json());
method: 'POST'
}).then(r => r.json());
export const adminGetItems = () => export const adminGetItems = () =>
fetch(`${BASE}/admin/items`) fetch(`${BASE}/admin/items`).then(r => r.json());
.then(r => r.json());
export const createItem = (data) => export const createItem = (title) =>
fetch(`${BASE}/admin/items`, { fetch(`${BASE}/admin/items`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({ title })
},
body: JSON.stringify(data)
}).then(r => r.json());
export const updateItem = (id, data) =>
fetch(`${BASE}/admin/items/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(r => r.json()); }).then(r => r.json());
export const deleteItem = (id) => export const deleteItem = (id) =>
fetch(`${BASE}/admin/items/${id}`, { fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(r => r.json());
method: 'DELETE'
}).then(r => {
if (r.status === 204) return {};
return r.json();
});

View File

@@ -4,309 +4,43 @@ const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const btn = document.getElementById('spin-btn'); const btn = document.getElementById('spin-btn');
const result = document.getElementById('result'); const result = document.getElementById('result');
const colors = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c'];
const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' };
const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' };
let items = []; let items = [];
let angle = 0; let angle = 0;
let spinning = false; let spinning = false;
let lampOn = true;
async function init() { async function init() {
items = await getItems(); items = await getItems();
draw(angle); drawWheel(angle);
setInterval(() => {
lampOn = !lampOn;
draw(angle);
}, 600);
} }
function getTheme(i, total) { function drawWheel(rotation) {
if (total % 2 === 0) {
return i % 2 === 0 ? DARK : LIGHT;
} else {
if (i === total - 1) return DARK;
return i % 2 === 0 ? DARK : LIGHT;
}
}
function drawSegment(cx, cy, r, start, end, theme) {
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, start, end);
ctx.closePath();
ctx.fillStyle = theme.edge;
ctx.fill();
const mid = (start + end) / 2;
const gx = cx + r * 0.5 * Math.cos(mid);
const gy = cy + r * 0.5 * Math.sin(mid);
const grad = ctx.createRadialGradient(gx, gy, 0, cx, cy, r);
grad.addColorStop(0, theme.highlight);
grad.addColorStop(0.45, theme.main);
grad.addColorStop(1, theme.edge);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r - 2, start, end);
ctx.closePath();
ctx.fillStyle = grad;
ctx.fill();
const hGrad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.35);
hGrad.addColorStop(0, 'rgba(255,255,255,0.28)');
hGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r - 2, start, end);
ctx.closePath();
ctx.fillStyle = hGrad;
ctx.fill();
}
function draw(rotation) {
if (items.length === 0) return; if (items.length === 0) return;
const cx = canvas.width / 2; const cx = canvas.width / 2;
const cy = canvas.height / 2; const cy = canvas.height / 2;
const r = cx - 40; const r = cx - 10;
const slice = (2 * Math.PI) / items.length; const slice = (2 * Math.PI) / items.length;
const total = items.length;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// ═══ سایه کلی ═══
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 30;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 8;
ctx.beginPath();
ctx.arc(cx, cy, r + 40, 0, Math.PI * 2);
ctx.fillStyle = '#1A0008';
ctx.fill();
ctx.restore();
// ═══ حلقه بیرونی تیره ═══
const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40);
rimGrad.addColorStop(0, '#A0003A');
rimGrad.addColorStop(0.3, '#6D0025');
rimGrad.addColorStop(0.7, '#3D0015');
rimGrad.addColorStop(1, '#1A0008');
ctx.beginPath();
ctx.arc(cx, cy, r + 40, 0, Math.PI * 2);
ctx.fillStyle = rimGrad;
ctx.fill();
// ═══ بخش‌های گردونه ═══
items.forEach((item, i) => { items.forEach((item, i) => {
const start = rotation + i * slice; const start = rotation + i * slice;
const end = start + slice; const end = start + slice;
drawSegment(cx, cy, r, start, end, getTheme(i, total));
});
// ═══ خطوط طلایی ═══
items.forEach((_, i) => {
const lineAngle = rotation + i * slice;
const lGrad = ctx.createLinearGradient(
cx, cy,
cx + r * Math.cos(lineAngle),
cy + r * Math.sin(lineAngle)
);
lGrad.addColorStop(0, 'rgba(700,550,0,5)');
lGrad.addColorStop(0.4, '#a2922e');
lGrad.addColorStop(1, '#c8b43b');
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(cx, cy); ctx.moveTo(cx, cy);
ctx.lineTo(cx + r * Math.cos(lineAngle), cy + r * Math.sin(lineAngle)); ctx.arc(cx, cy, r, start, end);
ctx.strokeStyle = lGrad; ctx.fillStyle = colors[i % colors.length];
ctx.lineWidth = 5; ctx.fill();
ctx.stroke(); ctx.stroke();
});
// ═══ متن آیتم‌ها ═══
items.forEach((item, i) => {
const start = rotation + i * slice;
const theme = getTheme(i, total);
ctx.save(); ctx.save();
ctx.translate(cx, cy); ctx.translate(cx, cy);
ctx.rotate(start + slice / 2); ctx.rotate(start + slice / 2);
ctx.textAlign = 'right'; ctx.textAlign = 'right';
ctx.fillStyle = theme.text; ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Tahoma'; ctx.font = 'bold 16px Tahoma';
ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.fillText(item.title, r - 10, 5);
ctx.shadowBlur = 4;
const text = item.title;
const maxWidth = r - 52;
if (ctx.measureText(text).width > maxWidth) {
const mid = Math.ceil(text.length / 2);
ctx.fillText(text.slice(0, mid), r - 20, -7);
ctx.fillText(text.slice(mid), r - 20, 9);
} else {
ctx.fillText(text, r - 20, 5);
}
ctx.restore(); ctx.restore();
}); });
// ═══ حلقه طلایی داخلی ═══
ctx.beginPath();
ctx.arc(cx, cy, r + 2, 0, Math.PI * 2);
ctx.strokeStyle = '#dfd498';
ctx.lineWidth = 4.5;
ctx.shadowColor = 'rgba(255,215,0,0.9)';
ctx.shadowBlur = 10;
ctx.stroke();
ctx.shadowBlur = 0;
// ═══ لامپ‌ها ═══
const lampCount = 24;
const lampR = r + 23;
for (let i = 0; i < lampCount; i++) {
const lampAngle = (i / lampCount) * Math.PI * 2;
const lx = cx + lampR * Math.cos(lampAngle);
const ly = cy + lampR * Math.sin(lampAngle);
const isOn = lampOn ? i % 2 === 0 : i % 2 !== 0;
if (isOn) {
const glowGrad = ctx.createRadialGradient(lx, ly, 0, lx, ly, 14);
glowGrad.addColorStop(0, 'rgba(255,220,80,0.55)');
glowGrad.addColorStop(1, 'rgba(255,220,80,0)');
ctx.beginPath();
ctx.arc(lx, ly, 14, 0, Math.PI * 2);
ctx.fillStyle = glowGrad;
ctx.fill();
}
const lGrad = ctx.createRadialGradient(lx - 2.5, ly - 2.5, 0.5, lx, ly, 7.5);
if (isOn) {
lGrad.addColorStop(0, '#FFFFF5');
lGrad.addColorStop(0.2, '#FFE566');
lGrad.addColorStop(0.6, '#FFB300');
lGrad.addColorStop(1, '#E65100');
ctx.shadowColor = 'rgba(255,180,0,1)';
ctx.shadowBlur = 14;
} else {
lGrad.addColorStop(0, '#B8954A');
lGrad.addColorStop(1, '#4A3010');
}
ctx.beginPath();
ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
ctx.fillStyle = lGrad;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(lx, ly, 7.5, 0, Math.PI * 2);
ctx.strokeStyle = isOn ? '#C8960C' : '#6A4A18';
ctx.lineWidth = 1.2;
ctx.stroke();
ctx.beginPath();
ctx.arc(lx - 2.5, ly - 2.5, 2.5, 0, Math.PI * 2);
ctx.fillStyle = isOn ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.2)';
ctx.fill();
}
// ═══ حلقه طلایی بیرونی ═══
ctx.beginPath();
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
ctx.strokeStyle = '#C8960C';
ctx.lineWidth = 4.5;
ctx.shadowColor = 'rgba(150,100,12,0.5)';
ctx.shadowBlur = 6;
ctx.stroke();
ctx.shadowBlur = 0;
// ═══ دایره مرکزی — هماهنگ با نشانگر ═══
const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22);
cGrad.addColorStop(0, '#FFFFF0');
cGrad.addColorStop(0.15, '#FFE566');
cGrad.addColorStop(0.45, '#FFD700');
cGrad.addColorStop(0.75, '#ff962a');
cGrad.addColorStop(1, '#b37900');
ctx.beginPath();
ctx.arc(cx, cy, 22, 0, Math.PI * 2);
ctx.fillStyle = cGrad;
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 14;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(cx, cy, 22, 0, Math.PI * 2);
ctx.strokeStyle = '#8B6914';
ctx.lineWidth = 2;
ctx.stroke();
// بازتاب روی دایره مرکزی
ctx.beginPath();
ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fill();
// ═══ نشانگر ═══
drawPointer(cx);
}
function drawPointer(cx) {
// سایه
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 4;
ctx.beginPath();
ctx.arc(cx, 20, 17, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.1)';
ctx.fill();
ctx.restore();
// دایره طلایی — همرنگ مرکز
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
pGrad.addColorStop(0, '#FFFFF0');
pGrad.addColorStop(0.15, '#FFE566');
pGrad.addColorStop(0.45, '#FFD700');
pGrad.addColorStop(0.75, '#ff962a');
pGrad.addColorStop(1, '#b37900');
ctx.beginPath();
ctx.arc(cx, 20, 17, 0, Math.PI * 2);
ctx.fillStyle = pGrad;
ctx.shadowColor = 'rgba(200,150,0,0.8)';
ctx.shadowBlur = 12;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#8B6914';
ctx.lineWidth = 2;
ctx.stroke();
// بازتاب
ctx.beginPath();
ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fill();
// مثلث — همرنگ
const triGrad = ctx.createLinearGradient(cx, 34, cx, 62);
triGrad.addColorStop(0, '#FFE566');
triGrad.addColorStop(0.35, '#FFD700');
triGrad.addColorStop(1, '#b37900');
ctx.beginPath();
ctx.moveTo(cx - 13, 35);
ctx.lineTo(cx + 13, 35);
ctx.lineTo(cx, 62);
ctx.closePath();
ctx.fillStyle = triGrad;
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#8B6914';
ctx.lineWidth = 1.5;
ctx.stroke();
} }
btn.onclick = async () => { btn.onclick = async () => {
@@ -319,30 +53,26 @@ btn.onclick = async () => {
const winner = data.winner; const winner = data.winner;
const winnerIdx = items.findIndex(i => i.id === winner.id); const winnerIdx = items.findIndex(i => i.id === winner.id);
const slice = (2 * Math.PI) / items.length; const slice = (2 * Math.PI) / items.length;
const targetAngle = angle + (Math.PI * 2 * 5) +
(Math.PI * 2 - (winnerIdx * slice + slice / 2));
const targetAngle = const duration = 4000;
angle + const start = performance.now();
Math.PI * 2 * 8 +
(-Math.PI / 2 - (winnerIdx * slice + slice / 2) - angle % (Math.PI * 2) + Math.PI * 2 * 3);
const duration = 5500;
const startTime = performance.now();
const from = angle; const from = angle;
function animate(now) { function animate(now) {
const elapsed = now - startTime; const elapsed = now - start;
const t = Math.min(elapsed / duration, 1); const t = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - t, 4); const ease = 1 - Math.pow(1 - t, 3);
angle = from + (targetAngle - from) * ease; angle = from + (targetAngle - from) * ease;
draw(angle); drawWheel(angle);
if (t < 1) { if (t < 1) {
requestAnimationFrame(animate); requestAnimationFrame(animate);
} else { } else {
spinning = false; spinning = false;
btn.disabled = false; btn.disabled = false;
draw(angle); result.textContent = `🎉 ${winner.title}`;
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
} }
} }

View File

@@ -6,14 +6,12 @@
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body> <body>
<div class="page"> <h1>گردونه شانس</h1>
<h1>گردونه شانس</h1> <canvas id="wheel" width="400" height="400"></canvas>
<div class="wheel-wrapper"> <br>
<canvas id="wheel" width="500" height="500"></canvas> <button id="spin-btn">بچرخون!</button>
</div> <p id="result"></p>
<button id="spin-btn">بچرخون!</button>
<div id="result"></div>
</div>
<script type="module" src="assets/js/wheel.js"></script> <script type="module" src="assets/js/wheel.js"></script>
</body> </body>
</html> </html>