git add .
git commit -m "feat: improve wheel UI with gold pointer and center circle sync"
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
DATABASE_URL=sqlite:///./wheel.db
|
DATABASE_URL=sqlite:///./spinwheel.db
|
||||||
|
|
||||||
LOG_ENABLED=true
|
LOG_ENABLED=true
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
backend/alembic/versions/b55808a86947_add_prize_column.py
Normal file
28
backend/alembic/versions/b55808a86947_add_prize_column.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""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
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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.error(f"Error fetching items: {e}")
|
logger.critical(f"Unexpected 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)
|
result = crud.create_item(db, item.title, item.value) # ✅ value اضافه شد
|
||||||
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.error(f"Error creating item: {e}")
|
logger.critical(f"Unexpected error creating item: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -65,5 +65,29 @@ def delete_item(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting item: {e}")
|
logger.critical(f"Unexpected 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
|
||||||
|
|
||||||
|
|
||||||
@@ -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.error(f"Error fetching items: {e}")
|
logger.critical(f"Unexpected error fetching items: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@@ -38,4 +38,26 @@ 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
|
||||||
Binary file not shown.
@@ -19,3 +19,8 @@ class Item(Base):
|
|||||||
String,
|
String,
|
||||||
nullable=False
|
nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
value = Column(
|
||||||
|
Integer,
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
|
|
||||||
DATABASE_URL: str
|
class Settings(BaseSettings):
|
||||||
|
DATABASE_URL: str = Field(..., alias="DATABASE_URL")
|
||||||
|
|
||||||
|
LOG_ENABLED: bool = Field(True, alias="LOG_ENABLED")
|
||||||
|
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()
|
||||||
Binary file not shown.
@@ -1 +1,289 @@
|
|||||||
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: تخفيف سه ماهه
|
||||||
|
2026-05-30 11:40:34 | INFO | admin | Items fetched: 8 items
|
||||||
|
2026-05-30 11:40:38 | INFO | admin | Items fetched: 8 items
|
||||||
|
2026-05-30 11:40:41 | INFO | admin | Items fetched: 8 items
|
||||||
|
2026-05-30 11:41:08 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:11 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:48:14 | INFO | user | Spin result: پوچ
|
||||||
|
2026-05-30 11:50:10 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:50:45 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:52:01 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:52:53 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:52:58 | INFO | user | Spin result: 10% تخفيف يک ماهه
|
||||||
|
2026-05-30 11:54:03 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:54:47 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:56:34 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:56:38 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:56:54 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:57:12 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 11:58:04 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:02:35 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:02:45 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:03:29 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:04:32 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:05:09 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:06:05 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:06:57 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:07:43 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:10:41 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:13:15 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:13:50 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:15:50 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:16:21 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:17:21 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:17:45 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:18:16 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:19:04 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:19:18 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:19:33 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:19:54 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:21:31 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:22:30 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:23:33 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:24:29 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:24:49 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:25:02 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:26:12 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:26:23 | INFO | user | Spin result: تخفيف يک ماهه
|
||||||
|
2026-05-30 12:27:13 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:27:39 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:28:21 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:28:44 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:29:09 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:29:31 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:29:55 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:30:01 | INFO | user | Spin result: پوچ
|
||||||
|
2026-05-30 12:30:51 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:31:19 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:31:40 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:32:00 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:32:23 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:32:54 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:33:20 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:35:04 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:35:31 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:35:48 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:36:41 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:37:39 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:38:22 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:38:45 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:39:00 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:43:07 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:43:19 | INFO | user | Spin result: تخفيف يک ماهه
|
||||||
|
2026-05-30 12:45:27 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:49:55 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:50:43 | INFO | admin | Items fetched: 8 items
|
||||||
|
2026-05-30 12:51:00 | INFO | user | User fetched items: 8 items
|
||||||
|
2026-05-30 12:53:57 | INFO | user | Spin result: پوچ
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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)
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
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")
|
||||||
|
|
||||||
app = FastAPI()
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
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
|
||||||
Binary file not shown.
@@ -1,61 +1,35 @@
|
|||||||
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
|
||||||
def create_item(
|
item = Item(title=title, value=value) # ✅ prize → value
|
||||||
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
|
||||||
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:
|
||||||
|
item.title = title
|
||||||
db.delete(item)
|
item.value = value # ✅ prize → value
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
db.refresh(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
def delete_item(db: Session, item_id: int):
|
||||||
|
item = db.query(Item).filter(Item.id == item_id).first()
|
||||||
|
if item:
|
||||||
|
db.delete(item)
|
||||||
|
db.commit()
|
||||||
|
return item
|
||||||
|
|
||||||
def spin_wheel(
|
def spin_wheel(db: Session):
|
||||||
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)
|
||||||
Binary file not shown.
@@ -1,20 +1,92 @@
|
|||||||
<!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>
|
|
||||||
<h1>مدیریت آیتمها</h1>
|
|
||||||
|
|
||||||
<div id="add-section">
|
<body>
|
||||||
<input type="text" id="new-item" placeholder="نام آیتم جدید">
|
|
||||||
<button id="add-btn">اضافه کن</button>
|
<div class="admin-container">
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<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>
|
||||||
@@ -1,61 +1,325 @@
|
|||||||
|
/* =========================
|
||||||
|
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;
|
||||||
padding: 40px 20px;
|
gap: 24px;
|
||||||
gap: 20px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.page h1 {
|
||||||
color: #2c3e50;
|
color: #FFD700;
|
||||||
margin-bottom: 10px;
|
font-size: 36px;
|
||||||
|
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
.wheel-wrapper {
|
||||||
border-radius: 50%;
|
position: relative;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.wheel-wrapper canvas {
|
||||||
padding: 12px 30px;
|
display: block;
|
||||||
font-size: 18px;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spin-btn {
|
||||||
|
padding: 16px 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
font-family: Tahoma, sans-serif;
|
font-family: Tahoma, sans-serif;
|
||||||
background: #e74c3c;
|
background: linear-gradient(135deg, #FFD700, #D4AC0D);
|
||||||
color: white;
|
color: #4a0f0f;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 50px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4);
|
||||||
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
#spin-btn:hover {
|
||||||
background: #c0392b;
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
#spin-btn:disabled {
|
||||||
background: #aaa;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#result {
|
#result {
|
||||||
font-size: 24px;
|
min-height: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-text {
|
||||||
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2c3e50;
|
color: #FFD700;
|
||||||
|
text-shadow: 0 0 20px rgba(255, 215, 0, 0.7);
|
||||||
|
animation: pop 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ادمین */
|
@keyframes pop {
|
||||||
#add-section {
|
0% { transform: scale(0.5); opacity: 0; }
|
||||||
display: flex;
|
70% { transform: scale(1.1); }
|
||||||
gap: 10px;
|
100% { transform: scale(1); opacity: 1; }
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
@media (max-width: 550px) {
|
||||||
padding: 10px;
|
.wheel-wrapper {
|
||||||
|
width: 340px;
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spin-btn {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,103 @@
|
|||||||
import { adminGetItems, createItem, deleteItem } from './api.js';
|
import {
|
||||||
|
adminGetItems,
|
||||||
|
createItem,
|
||||||
|
deleteItem,
|
||||||
|
updateItem // 👈 باید اینو هم به api.js اضافه کنی
|
||||||
|
} from './api.js';
|
||||||
|
|
||||||
const list = document.getElementById('items-list');
|
const tableBody = document.getElementById('items-table-body');
|
||||||
const input = document.getElementById('new-item');
|
const titleInput = document.getElementById('title-input');
|
||||||
|
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 = '';
|
|
||||||
items.forEach(item => {
|
tableBody.innerHTML = '';
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
items.forEach((item, index) => {
|
||||||
<span>${item.title}</span>
|
const row = document.createElement('tr');
|
||||||
<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 = input.value.trim();
|
const title = titleInput.value.trim();
|
||||||
if (!title) return;
|
const value = valueInput.value.trim();
|
||||||
await createItem(title);
|
|
||||||
input.value = '';
|
if (!title || !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();
|
||||||
@@ -1,20 +1,40 @@
|
|||||||
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`).then(r => r.json());
|
fetch(`${BASE}/user/items`)
|
||||||
|
.then(r => r.json());
|
||||||
|
|
||||||
export const spinWheel = () =>
|
export const spinWheel = () =>
|
||||||
fetch(`${BASE}/user/spin`, { method: 'POST' }).then(r => r.json());
|
fetch(`${BASE}/user/spin`, {
|
||||||
|
method: 'POST'
|
||||||
|
}).then(r => r.json());
|
||||||
|
|
||||||
export const adminGetItems = () =>
|
export const adminGetItems = () =>
|
||||||
fetch(`${BASE}/admin/items`).then(r => r.json());
|
fetch(`${BASE}/admin/items`)
|
||||||
|
.then(r => r.json());
|
||||||
|
|
||||||
export const createItem = (title) =>
|
export const createItem = (data) =>
|
||||||
fetch(`${BASE}/admin/items`, {
|
fetch(`${BASE}/admin/items`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({ title })
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
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}`, { method: 'DELETE' }).then(r => r.json());
|
fetch(`${BASE}/admin/items/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(r => {
|
||||||
|
if (r.status === 204) return {};
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
@@ -4,43 +4,309 @@ 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();
|
||||||
drawWheel(angle);
|
draw(angle);
|
||||||
|
setInterval(() => {
|
||||||
|
lampOn = !lampOn;
|
||||||
|
draw(angle);
|
||||||
|
}, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawWheel(rotation) {
|
function getTheme(i, total) {
|
||||||
|
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 - 10;
|
const r = cx - 40;
|
||||||
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.arc(cx, cy, r, start, end);
|
ctx.lineTo(cx + r * Math.cos(lineAngle), cy + r * Math.sin(lineAngle));
|
||||||
ctx.fillStyle = colors[i % colors.length];
|
ctx.strokeStyle = lGrad;
|
||||||
ctx.fill();
|
ctx.lineWidth = 5;
|
||||||
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 = '#fff';
|
ctx.fillStyle = theme.text;
|
||||||
ctx.font = 'bold 16px Tahoma';
|
ctx.font = 'bold 12px Tahoma';
|
||||||
ctx.fillText(item.title, r - 10, 5);
|
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||||
|
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 () => {
|
||||||
@@ -53,26 +319,30 @@ 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 duration = 4000;
|
const targetAngle =
|
||||||
const start = performance.now();
|
angle +
|
||||||
|
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 - start;
|
const elapsed = now - startTime;
|
||||||
const t = Math.min(elapsed / duration, 1);
|
const t = Math.min(elapsed / duration, 1);
|
||||||
const ease = 1 - Math.pow(1 - t, 3);
|
const ease = 1 - Math.pow(1 - t, 4);
|
||||||
angle = from + (targetAngle - from) * ease;
|
angle = from + (targetAngle - from) * ease;
|
||||||
drawWheel(angle);
|
draw(angle);
|
||||||
|
|
||||||
if (t < 1) {
|
if (t < 1) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
} else {
|
} else {
|
||||||
spinning = false;
|
spinning = false;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
result.textContent = `🎉 ${winner.title}`;
|
draw(angle);
|
||||||
|
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>گردونه شانس</h1>
|
<div class="page">
|
||||||
<canvas id="wheel" width="400" height="400"></canvas>
|
<h1>گردونه شانس</h1>
|
||||||
<br>
|
<div class="wheel-wrapper">
|
||||||
<button id="spin-btn">بچرخون!</button>
|
<canvas id="wheel" width="500" height="500"></canvas>
|
||||||
<p id="result"></p>
|
</div>
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user