Compare commits

...

2 Commits

Author SHA1 Message Date
df3ccb1886 feat: implement advanced reward management and wheel customization
- Add customizable background color
- Add wheel animations and sound effects
- Add reward description field and winner modal
- Change font to Dana
- Add configurable reward probabilities
- Support reward types (Empty, Discount Code, LiveCoin)
- Replace reward title with reward type selector
- Allow wheel page title customization from admin panel
2026-06-10 15:41:08 +03:30
ec86cdc528 feat: implement advanced reward management and wheel customization
- Add customizable background color
- Add wheel animations and sound effects
- Add reward description field and winner modal
- Change font to Dana
- Add configurable reward probabilities
- Support reward types (Empty, Discount Code, LiveCoin)
- Replace reward title with reward type selector
- Allow wheel page title customization from admin panel
2026-06-10 14:55:07 +03:30
26 changed files with 1260 additions and 533 deletions

View File

@@ -0,0 +1,28 @@
"""update items table
Revision ID: eb35214a1411
Revises: f71dc2bbdfc6
Create Date: 2026-06-03 02:15:20.584639
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'eb35214a1411'
down_revision: Union[str, Sequence[str], None] = 'f71dc2bbdfc6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,28 @@
"""update items table
Revision ID: f71dc2bbdfc6
Revises: b55808a86947
Create Date: 2026-06-03 02:12:35.370477
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f71dc2bbdfc6'
down_revision: Union[str, Sequence[str], None] = 'b55808a86947'
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

@@ -1,9 +1,5 @@
from fastapi import APIRouter from fastapi import APIRouter, Depends, HTTPException
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import schemas import schemas
from schemas import ItemResponse from schemas import ItemResponse
from services import crud from services import crud
@@ -13,7 +9,6 @@ from core.logger import get_logger
router = APIRouter() router = APIRouter()
logger = get_logger('admin') logger = get_logger('admin')
def get_db(): def get_db():
db = SessionLocal() db = SessionLocal()
try: try:
@@ -21,7 +16,6 @@ def get_db():
finally: finally:
db.close() db.close()
# GET ALL ITEMS # GET ALL ITEMS
@router.get("/items", response_model=list[ItemResponse]) @router.get("/items", response_model=list[ItemResponse])
def get_items(db: Session = Depends(get_db)): def get_items(db: Session = Depends(get_db)):
@@ -33,28 +27,51 @@ def get_items(db: Session = Depends(get_db)):
logger.critical(f"Unexpected error fetching items: {e}") logger.critical(f"Unexpected error fetching items: {e}")
raise raise
# CREATE ITEM # CREATE ITEM
@router.post("/items", response_model=ItemResponse) @router.post("/items", response_model=ItemResponse)
def create_item( def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
try: try:
result = crud.create_item(db, item.title, item.value) # ✅ value اضافه شد result = crud.create_item(
db,
item.title,
item.prize,
item.description,
item.prize_type,
item.probability
)
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.critical(f"Unexpected error creating item: {e}")
raise raise
# UPDATE ITEM
@router.put("/items/{item_id}", response_model=ItemResponse)
def update_item(item_id: int, item: schemas.ItemUpdate, db: Session = Depends(get_db)):
try:
updated = crud.update_item(
db,
item_id,
item.title,
item.prize,
item.description,
item.prize_type,
item.probability
)
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
# DELETE ITEM # DELETE ITEM
@router.delete("/items/{item_id}") @router.delete("/items/{item_id}")
def delete_item( def delete_item(item_id: int, db: Session = Depends(get_db)):
item_id: int,
db: Session = Depends(get_db)
):
try: try:
item = crud.delete_item(db, item_id) item = crud.delete_item(db, item_id)
if not item: if not item:
@@ -67,27 +84,3 @@ def delete_item(
except Exception as e: except Exception as e:
logger.critical(f"Unexpected 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

View File

@@ -51,7 +51,10 @@ def spin(db: Session = Depends(get_db)):
return { return {
"winner": { "winner": {
"id": winner.id, "id": winner.id,
"title": winner.title "title": winner.title,
"prize": winner.prize,
"prize_type": winner.prize_type,
"description": winner.description or ""
} }
} }

View File

@@ -1,26 +1,12 @@
from sqlalchemy import Column from sqlalchemy import Column, Integer, String, Float
from sqlalchemy import Integer
from sqlalchemy import String
from app.db.session import Base from app.db.session import Base
class Item(Base): class Item(Base):
__tablename__ = "items" __tablename__ = "items"
id = Column( id = Column(Integer, primary_key=True, index=True)
Integer, title = Column(String, nullable=False)
primary_key=True, prize = Column(String, nullable=False)
index=True description = Column(String, nullable=True, default="")
) prize_type = Column(String, nullable=False, default="پوچ")
probability = Column(Float, nullable=False, default=1.0)
title = Column(
String,
nullable=False
)
value = Column(
Integer,
nullable=False
)

View File

@@ -287,3 +287,231 @@ FROM items]
2026-05-30 12:50:43 | INFO | admin | Items fetched: 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:51:00 | INFO | user | User fetched items: 8 items
2026-05-30 12:53:57 | INFO | user | Spin result: پوچ 2026-05-30 12:53:57 | INFO | user | Spin result: پوچ
2026-05-30 14:30:41 | INFO | admin | Items fetched: 8 items
2026-05-30 14:31:19 | INFO | user | User fetched items: 8 items
2026-05-30 14:31:21 | INFO | user | Spin result: تخفيف يک ماهه
2026-05-30 14:32:54 | INFO | user | Spin result: پوچ
2026-05-30 14:33:18 | INFO | admin | Item updated: 1
2026-05-30 14:33:18 | INFO | admin | Items fetched: 8 items
2026-05-30 14:33:38 | INFO | user | Spin result: تخفيف سه ماهه
2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:26:33 | INFO | user | User fetched items: 0 items
2026-06-07 00:32:56 | INFO | user | User fetched items: 0 items
2026-06-07 00:36:17 | INFO | user | User fetched items: 0 items
2026-06-07 00:36:21 | INFO | user | User fetched items: 0 items
2026-06-07 00:39:11 | INFO | user | User fetched items: 0 items
2026-06-07 00:39:15 | INFO | admin | Items fetched: 0 items
2026-06-07 00:39:33 | INFO | admin | Items fetched: 0 items
2026-06-07 00:49:16 | INFO | admin | Items fetched: 0 items
2026-06-07 00:49:39 | INFO | admin | Item created: jotdt 20
2026-06-07 00:49:39 | INFO | admin | Items fetched: 1 items
2026-06-07 00:49:44 | INFO | user | User fetched items: 1 items
2026-06-07 00:49:47 | INFO | user | Spin result: jotdt 20
2026-06-07 00:50:31 | INFO | admin | Item created: پوچ
2026-06-07 00:50:31 | INFO | admin | Items fetched: 2 items
2026-06-07 00:50:38 | INFO | user | User fetched items: 2 items
2026-06-07 00:50:40 | INFO | user | Spin result: پوچ
2026-06-07 00:51:12 | INFO | admin | Item created: تخفيف 20
2026-06-07 00:51:12 | INFO | admin | Items fetched: 3 items
2026-06-07 00:51:16 | INFO | user | User fetched items: 3 items
2026-06-07 00:52:18 | INFO | user | Spin result: پوچ
2026-06-07 00:53:54 | INFO | admin | Item updated: 1
2026-06-07 00:53:54 | INFO | admin | Items fetched: 3 items
2026-06-07 00:54:22 | INFO | admin | Item created: تخفيف 30
2026-06-07 00:54:22 | INFO | admin | Items fetched: 4 items
2026-06-07 00:54:35 | INFO | user | User fetched items: 4 items
2026-06-07 00:54:38 | INFO | user | Spin result: پوچ
2026-06-07 00:55:39 | INFO | user | Spin result: تخفيف 30
2026-06-07 00:56:16 | INFO | admin | Item created: تخفيف 30
2026-06-07 00:56:16 | INFO | admin | Items fetched: 5 items
2026-06-07 00:56:51 | INFO | admin | Item created: پوچ
2026-06-07 00:56:51 | INFO | admin | Items fetched: 6 items
2026-06-07 00:56:58 | INFO | user | User fetched items: 6 items
2026-06-07 00:57:01 | INFO | user | Spin result: پوچ
2026-06-07 00:57:13 | INFO | user | Spin result: پوچ
2026-06-07 01:03:09 | INFO | user | User fetched items: 6 items
2026-06-07 01:03:12 | INFO | user | Spin result: پوچ
2026-06-07 01:07:35 | INFO | admin | Items fetched: 6 items
2026-06-07 01:07:37 | INFO | user | User fetched items: 6 items
2026-06-07 01:07:39 | INFO | user | Spin result: تخفيف 20
2026-06-07 02:15:44 | INFO | user | User fetched items: 6 items
2026-06-07 02:15:45 | INFO | user | Spin result: پوچ
2026-06-07 02:19:11 | INFO | user | Spin result: پوچ
2026-06-07 02:19:40 | INFO | user | Spin result: پوچ
2026-06-07 13:34:50 | INFO | user | User fetched items: 6 items
2026-06-07 13:34:52 | INFO | user | Spin result: پوچ
2026-06-07 13:35:15 | INFO | admin | Items fetched: 6 items
2026-06-07 13:35:36 | INFO | user | Spin result: پوچ
2026-06-10 11:55:45 | INFO | user | User fetched items: 6 items
2026-06-10 11:56:01 | INFO | admin | Items fetched: 6 items
2026-06-10 11:57:45 | INFO | user | Spin result: پوچ
2026-06-10 11:59:13 | INFO | user | Spin result: پوچ
2026-06-10 11:59:33 | INFO | admin | Item updated: 3
2026-06-10 11:59:33 | INFO | admin | Items fetched: 6 items
2026-06-10 11:59:36 | INFO | user | Spin result: پوچ
2026-06-10 11:59:45 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:01:01 | INFO | user | Spin result: پوچ
2026-06-10 12:02:20 | INFO | user | Spin result: پوچ
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
2026-06-10 12:08:44 | INFO | user | User fetched items: 6 items
2026-06-10 12:08:47 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:17:29 | INFO | user | User fetched items: 6 items
2026-06-10 12:17:32 | INFO | user | Spin result: پوچ
2026-06-10 12:18:03 | INFO | user | Spin result: پوچ
2026-06-10 12:18:44 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:19:16 | INFO | user | User fetched items: 6 items
2026-06-10 12:19:20 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:20:37 | INFO | user | Spin result: پوچ
2026-06-10 12:20:41 | INFO | user | User fetched items: 6 items
2026-06-10 12:20:49 | INFO | admin | Items fetched: 6 items
2026-06-10 12:22:37 | INFO | user | User fetched items: 6 items
2026-06-10 12:22:39 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:22:49 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:23:08 | INFO | user | User fetched items: 6 items
2026-06-10 12:23:09 | INFO | user | Spin result: پوچ
2026-06-10 12:25:32 | INFO | user | User fetched items: 6 items
2026-06-10 12:25:34 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:30:55 | INFO | user | User fetched items: 6 items
2026-06-10 12:30:56 | INFO | user | Spin result: پوچ
2026-06-10 12:36:33 | INFO | user | User fetched items: 6 items
2026-06-10 12:36:35 | INFO | user | Spin result: تخفيف 20
2026-06-10 12:37:57 | INFO | user | User fetched items: 6 items
2026-06-10 12:48:20 | INFO | user | User fetched items: 6 items
2026-06-10 12:51:46 | INFO | user | User fetched items: 6 items
2026-06-10 12:58:36 | INFO | user | User fetched items: 6 items
2026-06-10 12:58:41 | INFO | user | Spin result: تخفيف 30
2026-06-10 12:59:01 | INFO | admin | Items fetched: 6 items
2026-06-10 12:59:04 | INFO | admin | Items fetched: 6 items
2026-06-10 13:00:57 | INFO | user | User fetched items: 6 items
2026-06-10 13:01:44 | INFO | user | User fetched items: 6 items
2026-06-10 13:01:46 | INFO | user | User fetched items: 6 items
2026-06-10 13:07:23 | INFO | user | User fetched items: 6 items
2026-06-10 13:07:25 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:07:51 | INFO | admin | Items fetched: 6 items
2026-06-10 13:09:29 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:12:47 | INFO | user | User fetched items: 6 items
2026-06-10 13:14:30 | INFO | user | User fetched items: 6 items
2026-06-10 13:14:34 | INFO | user | Spin result: پوچ
2026-06-10 13:15:38 | INFO | user | User fetched items: 6 items
2026-06-10 13:15:41 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:16:57 | INFO | user | User fetched items: 6 items
2026-06-10 13:18:40 | INFO | user | User fetched items: 6 items
2026-06-10 13:19:19 | INFO | user | User fetched items: 6 items
2026-06-10 13:19:21 | INFO | user | User fetched items: 6 items
2026-06-10 13:19:25 | INFO | admin | Items fetched: 6 items
2026-06-10 13:22:09 | INFO | user | User fetched items: 6 items
2026-06-10 13:22:17 | INFO | user | User fetched items: 6 items
2026-06-10 13:22:20 | INFO | admin | Items fetched: 6 items
2026-06-10 13:23:23 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:24:44 | INFO | user | User fetched items: 6 items
2026-06-10 13:25:17 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:27:10 | INFO | admin | Items fetched: 6 items
2026-06-10 13:28:35 | INFO | admin | Items fetched: 6 items
2026-06-10 13:29:16 | INFO | admin | Items fetched: 6 items
2026-06-10 13:29:41 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:30:47 | INFO | admin | Items fetched: 6 items
2026-06-10 13:31:14 | INFO | admin | Items fetched: 6 items
2026-06-10 13:31:41 | INFO | admin | Items fetched: 6 items
2026-06-10 13:32:39 | INFO | user | User fetched items: 6 items
2026-06-10 13:33:20 | INFO | user | User fetched items: 6 items
2026-06-10 13:35:04 | INFO | user | User fetched items: 6 items
2026-06-10 13:37:57 | INFO | user | User fetched items: 6 items
2026-06-10 13:39:21 | INFO | user | User fetched items: 6 items
2026-06-10 13:39:41 | INFO | user | User fetched items: 6 items
2026-06-10 13:39:53 | INFO | user | User fetched items: 6 items
2026-06-10 13:40:34 | INFO | user | User fetched items: 6 items
2026-06-10 13:42:08 | INFO | user | User fetched items: 6 items
2026-06-10 13:42:11 | INFO | user | Spin result: تخفيف 30
2026-06-10 13:44:29 | INFO | user | Spin result: پوچ
2026-06-10 13:48:32 | INFO | user | User fetched items: 6 items
2026-06-10 13:48:38 | INFO | admin | Items fetched: 6 items
2026-06-10 13:49:14 | INFO | user | Spin result: پوچ
2026-06-10 13:49:22 | INFO | user | Spin result: پوچ
2026-06-10 13:49:32 | INFO | user | Spin result: تخفيف 20
2026-06-10 13:50:41 | INFO | admin | Item updated: 3
2026-06-10 13:50:41 | INFO | admin | Items fetched: 6 items
2026-06-10 13:51:15 | INFO | user | Spin result: تخفيف 30
2026-06-10 13:51:53 | INFO | user | Spin result: پوچ
2026-06-10 13:53:38 | INFO | user | User fetched items: 6 items
2026-06-10 13:53:58 | INFO | user | Spin result: پوچ
2026-06-10 13:55:11 | INFO | user | Spin result: پوچ
2026-06-10 13:57:25 | INFO | user | Spin result: پوچ
2026-06-10 13:58:19 | INFO | user | User fetched items: 6 items
2026-06-10 13:58:48 | INFO | user | User fetched items: 6 items
2026-06-10 14:02:41 | INFO | user | User fetched items: 6 items
2026-06-10 14:02:44 | INFO | admin | Items fetched: 6 items
2026-06-10 14:03:24 | INFO | admin | Items fetched: 6 items
2026-06-10 14:04:02 | INFO | admin | Items fetched: 6 items
2026-06-10 14:04:47 | INFO | user | Spin result: پوچ
2026-06-10 14:06:37 | INFO | user | Spin result: پوچ
2026-06-10 14:07:42 | INFO | admin | Item updated: 3
2026-06-10 14:07:42 | INFO | admin | Items fetched: 6 items
2026-06-10 14:07:48 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:08:03 | INFO | user | Spin result: پوچ
2026-06-10 14:08:15 | INFO | admin | Item deleted: 6
2026-06-10 14:08:15 | INFO | admin | Items fetched: 5 items
2026-06-10 14:08:25 | INFO | admin | Item updated: 2
2026-06-10 14:08:25 | INFO | admin | Items fetched: 5 items
2026-06-10 14:08:30 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:08:38 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:08:47 | INFO | user | User fetched items: 5 items
2026-06-10 14:08:50 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:09:40 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:11:30 | INFO | user | User fetched items: 5 items
2026-06-10 14:12:04 | INFO | admin | Item created: تخفيف 20
2026-06-10 14:12:04 | INFO | admin | Items fetched: 6 items
2026-06-10 14:12:09 | INFO | user | User fetched items: 6 items
2026-06-10 14:12:16 | INFO | user | User fetched items: 6 items
2026-06-10 14:13:32 | INFO | user | User fetched items: 6 items
2026-06-10 14:13:34 | INFO | user | Spin result: تخفيف 20
2026-06-10 14:13:54 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:14:26 | INFO | user | Spin result: 1000 لایو کوین
2026-06-10 14:15:30 | INFO | admin | Items fetched: 6 items
2026-06-10 14:15:39 | INFO | user | User fetched items: 6 items
2026-06-10 14:16:38 | INFO | user | User fetched items: 6 items
2026-06-10 14:16:44 | INFO | admin | Items fetched: 6 items
2026-06-10 14:17:30 | INFO | admin | Items fetched: 6 items
2026-06-10 14:21:12 | INFO | user | Spin result: تخفيف 20
2026-06-10 14:21:20 | INFO | user | Spin result: تخفيف 30
2026-06-10 14:21:30 | INFO | user | Spin result: تخفيف 20
2026-06-10 14:43:11 | INFO | admin | Items fetched: 6 items
2026-06-10 14:45:05 | INFO | user | User fetched items: 6 items
2026-06-10 14:45:40 | INFO | user | User fetched items: 6 items
2026-06-10 14:46:11 | INFO | user | Spin result: تخفيف 30
2026-06-10 14:46:52 | INFO | user | User fetched items: 6 items

View File

@@ -38,3 +38,35 @@ FROM items]
[SQL: INSERT INTO items (title) VALUES (?)] [SQL: INSERT INTO items (title) VALUES (?)]
[parameters: ('جایزه 1',)] [parameters: ('جایزه 1',)]
(Background on this error at: https://sqlalche.me/e/20/e3q8) (Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
FROM items]
(Background on this error at: https://sqlalche.me/e/20/e3q8)

View File

@@ -1,19 +1,14 @@
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 engine, Base
from app.db.session import Base
from app.db.models import Item 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,
@@ -26,4 +21,8 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# ✅ بعد router ها
app.include_router(user_router, prefix="/user")
app.include_router(admin_router, prefix="/admin") app.include_router(admin_router, prefix="/admin")
Base.metadata.create_all(bind=engine)

View File

@@ -1,20 +1,27 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class ItemCreate(BaseModel): class ItemCreate(BaseModel):
title: str title: str
value: int prize: str
description: Optional[str] = ""
prize_type: str = "پوچ"
probability: float = 1.0
class ItemUpdate(BaseModel): class ItemUpdate(BaseModel):
title: str title: str
value: int prize: str
description: Optional[str] = ""
prize_type: str = "پوچ"
probability: float = 1.0
class ItemResponse(BaseModel): class ItemResponse(BaseModel):
id: int id: int
title: str title: str
value: int prize: str
description: Optional[str] = ""
prize_type: str
probability: float
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -5,18 +5,27 @@ 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(db: Session, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
item = Item(title=title, value=value) # ✅ prize → value item = Item(
title=title,
prize=prize,
description=description,
prize_type=prize_type,
probability=probability
)
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 update_item(db: Session, item_id: int, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
item = db.query(Item).filter(Item.id == item_id).first() item = db.query(Item).filter(Item.id == item_id).first()
if item: if item:
item.title = title item.title = title
item.value = value # ✅ prize → value item.prize = prize
item.description = description
item.prize_type = prize_type
item.probability = probability
db.commit() db.commit()
db.refresh(item) db.refresh(item)
return item return item
@@ -32,4 +41,5 @@ 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) weights = [item.probability for item in items]
return random.choices(items, weights=weights, k=1)[0]

Binary file not shown.

View File

@@ -1,92 +1,161 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>پنل مدیریت</title> <title>پنل مدیریت</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body id="main-body">
<body>
<div class="admin-container"> <div class="admin-container">
<div class="admin-header"> <div class="admin-header">
<h1> مدیریت گردونه شانس</h1> <h1>مدیریت گردونه شانس</h1>
<p>مدیریت آیتم‌ها و جوایز گردونه</p> <p>مدیریت آیتم‌ها و جوایز گردونه</p>
</div> </div>
<!-- تنظیمات رنگ پس‌زمینه -->
<div class="card">
<h2>تنظیمات ظاهری</h2>
<div class="color-section">
<label>رنگ پس‌زمینه:</label>
<div class="color-presets">
<div class="preset" style="background:#7B241C" data-color="#7B241C"></div>
<div class="preset" style="background:#1a237e" data-color="#1a237e"></div>
<div class="preset" style="background:#1b5e20" data-color="#1b5e20"></div>
<div class="preset" style="background:#4a148c" data-color="#4a148c"></div>
<div class="preset" style="background:#e65100" data-color="#e65100"></div>
<div class="preset" style="background:#006064" data-color="#006064"></div>
<div class="preset" style="background:#212121" data-color="#212121"></div>
<div class="preset" style="background:#880e4f" data-color="#880e4f"></div>
</div>
<div class="color-custom">
<label>رنگ دلخواه:</label>
<input type="color" id="bg-color-picker" value="#7B241C">
<button id="apply-color">اعمال</button>
</div>
<div class="color-preview">
<span>پیش‌نمایش:</span>
<div id="color-preview-box"></div>
</div>
</div>
</div>
<!-- عنوان صفحه گردونه -->
<div class="title-section">
<h3>عنوان صفحه گردونه</h3>
<div class="form-grid">
<div class="form-group">
<label>متن عنوان:</label>
<input type="text" id="title-text-input" placeholder="گردونه شانس">
</div>
<div class="form-group">
<label>رنگ عنوان:</label>
<div style="display:flex; align-items:center; gap:10px;">
<input type="color" id="title-color-picker" value="#FFD700">
<span id="title-preview" style="font-size:20px; font-weight:bold; color:#FFD700;">گردونه شانس</span>
</div>
</div>
<div class="form-group full-width">
<button id="apply-title" class="btn-primary">اعمال</button>
</div>
</div>
</div>
<!-- افزودن آیتم --> <!-- افزودن آیتم -->
<div class="add-section"> <div class="card">
<h2>افزودن آیتم جدید</h2>
<input <div class="form-grid">
type="text" <div class="form-group">
id="title-input" <label>نوع جایزه:</label>
placeholder="عنوان جایزه" <select id="new-prize-type">
> <option value="پوچ">پوچ</option>
<option value="کد تخفیف">کد تخفیف</option>
<input <option value="لایوکوین">لایوکوین (LC)</option>
type="number" </select>
id="value-input" </div>
placeholder="مقدار جایزه" <div class="form-group">
> <label>عنوان:</label>
<input type="text" id="new-title" placeholder="مثلاً: تخفیف ۲۰٪">
<button id="add-btn"> </div>
افزودن <div class="form-group">
</button> <label>مقدار جایزه:</label>
<input type="text" id="new-prize" placeholder="مثلاً: DISC20">
</div>
<div class="form-group">
<label>احتمال (۱ = عادی):</label>
<input type="number" id="new-probability" value="1" min="0.1" step="0.1">
</div>
<div class="form-group full-width">
<label>توضیحات:</label>
<textarea id="new-description" placeholder="توضیحاتی که بعد از برنده شدن نمایش داده میشه..."></textarea>
</div>
<div class="form-group full-width">
<button id="add-btn" class="btn-primary">افزودن آیتم</button>
</div>
</div>
</div> </div>
<!-- جدول --> <!-- جدول -->
<div class="table-wrapper"> <div class="card">
<h2>لیست آیتم‌ها</h2>
<table> <div class="table-wrapper">
<table>
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>عنوان جایزه</th> <th>نوع جایزه</th>
<th>مقدار</th> <th>عنوان</th>
<th>عملیات</th> <th>مقدار</th>
</tr> <th>احتمال</th>
</thead> <th>توضیحات</th>
<th>عملیات</th>
<tbody id="items-table-body"> </tr>
</thead>
<!-- ردیف نمونه (دینامیک توسط JS ساخته می‌شود) --> <tbody id="items-table-body"></tbody>
<!-- </table>
<tr> </div>
<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>
<!-- مودال ویرایش -->
<div id="edit-modal" class="modal hidden">
<div class="modal-content admin-modal">
<h2>ویرایش آیتم</h2>
<input type="hidden" id="edit-id">
<div class="form-grid">
<div class="form-group">
<label>نوع جایزه:</label>
<select id="edit-prize-type">
<option value="پوچ">پوچ</option>
<option value="کد تخفیف">کد تخفیف</option>
<option value="لایوکوین">لایوکوین (LC)</option>
</select>
</div>
<div class="form-group">
<label>عنوان:</label>
<input type="text" id="edit-title">
</div>
<div class="form-group">
<label>مقدار جایزه:</label>
<input type="text" id="edit-prize">
</div>
<div class="form-group">
<label>احتمال:</label>
<input type="number" id="edit-probability" min="0.1" step="0.1">
</div>
<div class="form-group full-width">
<label>توضیحات:</label>
<textarea id="edit-description"></textarea>
</div>
</div>
<div class="modal-actions">
<button id="save-edit" class="btn-primary">ذخیره</button>
<button id="cancel-edit" class="btn-secondary">انصراف</button>
</div>
</div>
</div> </div>
<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,261 +1,68 @@
/* ========================= @font-face {
ADMIN PANEL font-family: 'Dana';
========================= */ src: url('../fonts/DanaFaNum-Regular.ttf') format('truetype');
font-weight: 100 900;
.admin-container { font-style: normal;
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
========================= */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: 'Dana' !important;
} }
/* =========================
WHEEL PAGE
========================= */
body { body {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%); background: radial-gradient(ellipse at center, #9B241CDD 0%, #7B241C 55%, #7B241CBB 100%);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-family: Tahoma, sans-serif; direction: rtl;
}
.bg-rays {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200vw;
height: 200vh;
background: repeating-conic-gradient(
rgba(255,255,255,0.03) 0deg,
rgba(255,255,255,0.03) 10deg,
transparent 10deg,
transparent 20deg
);
pointer-events: none;
z-index: 0;
} }
.page { .page {
position: relative;
z-index: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 24px; gap: 28px;
padding: 30px; padding: 30px;
} }
.page h1 { .page h1 {
color: #FFD700; color: #e9e3c0;
font-size: 36px; font-size: 42px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); font-weight: bold;
text-shadow: 0 0 20px rgba(255,215,0,0.5);
letter-spacing: 2px; letter-spacing: 2px;
} }
.wheel-wrapper { .wheel-wrapper {
position: relative; position: relative;
width: 500px; width: 520px;
height: 500px; height: 520px;
} }
.wheel-wrapper canvas { .wheel-wrapper canvas {
@@ -265,22 +72,21 @@ body {
} }
#spin-btn { #spin-btn {
padding: 16px 48px; padding: 18px 56px;
font-size: 20px; font-size: 24px;
font-weight: bold; font-weight: bold;
font-family: Tahoma, sans-serif; background: linear-gradient(135deg, #e7d78a, #f2dd9d);
background: linear-gradient(135deg, #FFD700, #D4AC0D);
color: #4a0f0f; color: #4a0f0f;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
cursor: pointer; cursor: pointer;
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4); box-shadow: 0 6px 25px rgba(255,215,0,0.4);
transition: 0.2s; transition: 0.2s;
} }
#spin-btn:hover { #spin-btn:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55); box-shadow: 0 10px 30px rgba(255,215,0,0.55);
} }
#spin-btn:disabled { #spin-btn:disabled {
@@ -295,10 +101,10 @@ body {
} }
.winner-text { .winner-text {
font-size: 28px; font-size: 32px;
font-weight: bold; font-weight: bold;
color: #FFD700; color: #FFD700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.7); text-shadow: 0 0 20px rgba(255,215,0,0.7);
animation: pop 0.4s ease; animation: pop 0.4s ease;
} }
@@ -308,18 +114,380 @@ body {
100% { transform: scale(1); opacity: 1; } 100% { transform: scale(1); opacity: 1; }
} }
@media (max-width: 550px) { /* =========================
.wheel-wrapper { مودال برنده
width: 340px; ========================= */
height: 340px; .modal {
} position: fixed;
inset: 0;
.page h1 { background: rgba(0,0,0,0.75);
font-size: 26px; display: flex;
} align-items: center;
justify-content: center;
#spin-btn { z-index: 100;
font-size: 16px; }
padding: 12px 36px;
} .modal.hidden { display: none; }
.modal-content {
background: white;
border-radius: 20px;
padding: 44px 36px;
text-align: center;
max-width: 420px;
width: 90%;
animation: popIn 0.4s ease;
direction: rtl;
}
@keyframes popIn {
0% { transform: scale(0.5); opacity: 0; }
70% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.modal-icon { font-size: 70px; margin-bottom: 12px; }
.modal-content h2 {
color: #7B241C;
font-size: 30px;
font-weight: bold;
margin-bottom: 10px;
}
#modal-prize-type {
color: #888;
font-size: 17px;
margin-bottom: 8px;
}
#modal-prize {
color: #C0392B;
font-size: 26px;
font-weight: bold;
margin-bottom: 14px;
}
#modal-description {
color: #555;
font-size: 17px;
margin-bottom: 28px;
line-height: 2;
}
#modal-close {
padding: 14px 48px;
background: linear-gradient(135deg, #FFD700, #D4AC0D);
color: #4a0f0f;
border: none;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
}
/* =========================
ADMIN PANEL
========================= */
.admin-container {
width: 150%;
max-width: 1100px;
margin: 0 auto;
padding: 30px 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
.admin-header {
text-align: center;
margin-bottom: 20px;
}
.admin-header h1 {
color: #e9e8df;
font-size: 40px;
font-weight: bold;
text-shadow: 0 0 15px rgba(255,215,0,0.5);
}
.admin-header p {
color: #f5d6a0;
margin-top: 8px;
font-size: 18px;
}
.card {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.card h2 {
color: #efede1;
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.color-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.color-section label {
color: #f5d6a0;
font-size: 16px;
}
.color-presets {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.preset {
width: 44px;
height: 44px;
border-radius: 8px;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.2s, border-color 0.2s;
}
.preset:hover { transform: scale(1.15); }
.preset.active { border-color: #e5e3d5; }
.color-custom {
display: flex;
align-items: center;
gap: 12px;
}
.color-preview {
display: flex;
align-items: center;
gap: 12px;
color: #f5d6a0;
font-size: 16px;
}
#color-preview-box {
width: 120px;
height: 36px;
border-radius: 8px;
border: 2px solid rgba(255,255,255,0.2);
transition: background 0.3s;
}
input[type="color"] {
width: 50px;
height: 38px;
border: none;
border-radius: 6px;
cursor: pointer;
padding: 2px;
background: transparent;
}
#apply-color {
padding: 10px 20px;
background: rgba(255,255,255,0.15);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
cursor: pointer;
font-size: 15px;
transition: 0.2s;
}
#apply-color:hover { background: rgba(255,255,255,0.25); }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group.full-width { grid-column: 1 / -1; }
.form-group label {
font-size: 15px;
color: #f5d6a0;
font-weight: bold;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px 14px;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
font-size: 15px;
background: rgba(255,255,255,0.08);
color: white;
transition: border-color 0.2s;
}
.form-group input::placeholder,
.form-group textarea::placeholder {
color: rgba(255,255,255,0.4);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #e4e2d7;
}
.form-group select option {
background: #4a0f0f;
color: white;
}
.form-group textarea { height: 90px; resize: vertical; }
.btn-primary {
padding: 13px 32px;
background: linear-gradient(135deg, #e3dfb7, #ebe5be);
color: #4a0f0f;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
}
.btn-primary:hover { transform: translateY(-2px); }
.btn-secondary {
padding: 13px 32px;
background: rgba(255,255,255,0.1);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: 0.2s;
}
.btn-secondary:hover { background: rgba(255,255,255,0.2); }
.table-wrapper {
overflow-x: auto;
border-radius: 12px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 15px;
}
thead tr {
background: linear-gradient(135deg, #ebe3b9, #ebe1bc);
color: #0f374a;
}
th, td {
padding: 14px 16px;
text-align: right;
}
tbody tr {
border-bottom: 1px solid rgba(255,255,255,0.06);
color: white;
transition: 0.2s;
}
tbody tr:hover { background: rgba(255,255,255,0.05); }
.badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: bold;
}
.badge-pooch { background: rgba(255,255,255,0.1); color: #ccc; }
.badge-discount { background: rgba(46,125,50,0.3); color: #81C784; }
.badge-coin { background: rgba(245,127,23,0.3); color: #FFD700; }
.edit-btn {
padding: 7px 16px;
background: linear-gradient(135deg, #9B59B6, #7D3C98);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-left: 6px;
transition: 0.2s;
}
.delete-btn {
padding: 7px 16px;
background: linear-gradient(135deg, #E74C3C, #922B21);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: 0.2s;
}
.edit-btn:hover, .delete-btn:hover { transform: scale(1.05); }
.modal-content.admin-modal {
background: #2d0000;
color: white;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
.modal-content.admin-modal h2 {
color: #FFD700;
margin-bottom: 20px;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: flex-end;
}
@media (max-width: 550px) {
.wheel-wrapper { width: 340px; height: 340px; }
.page h1 { font-size: 28px; }
#spin-btn { font-size: 18px; padding: 14px 40px; }
.form-grid { grid-template-columns: 1fr; }
}
.title-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.title-section h3 {
color: #f5d6a0;
font-size: 15px;
margin-bottom: 14px;
font-family: 'Dana', Tahoma, sans-serif;
} }

Binary file not shown.

View File

@@ -1,103 +1,158 @@
import { import { adminGetItems, createItem, deleteItem, updateItem, saveBgColor, getBgColor, saveTitleSettings, getTitleSettings } from './api.js';
adminGetItems,
createItem,
deleteItem,
updateItem // 👈 باید اینو هم به api.js اضافه کنی
} from './api.js';
const tableBody = document.getElementById('items-table-body'); const tableBody = document.getElementById('items-table-body');
const titleInput = document.getElementById('title-input');
const valueInput = document.getElementById('value-input');
const addBtn = document.getElementById('add-btn'); const addBtn = document.getElementById('add-btn');
const editModal = document.getElementById('edit-modal');
const saveEdit = document.getElementById('save-edit');
const cancelEdit = document.getElementById('cancel-edit');
const colorPicker = document.getElementById('bg-color-picker');
const applyColor = document.getElementById('apply-color');
const previewBox = document.getElementById('color-preview-box');
const presets = document.querySelectorAll('.preset');
let editId = null; // 👈 حالت ویرایش // ═══ رنگ پس‌زمینه ═══
function initColor() {
// -------------------- Render -------------------- const saved = getBgColor();
async function renderItems() { colorPicker.value = saved;
const items = await adminGetItems(); previewBox.style.background = saved;
presets.forEach(p => {
tableBody.innerHTML = ''; if (p.dataset.color === saved) p.classList.add('active');
items.forEach((item, index) => {
const row = document.createElement('tr');
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>
`;
tableBody.appendChild(row);
}); });
} }
// -------------------- Delete -------------------- presets.forEach(p => {
p.addEventListener('click', () => {
presets.forEach(x => x.classList.remove('active'));
p.classList.add('active');
colorPicker.value = p.dataset.color;
previewBox.style.background = p.dataset.color;
});
});
colorPicker.addEventListener('input', () => {
previewBox.style.background = colorPicker.value;
presets.forEach(x => x.classList.remove('active'));
});
applyColor.addEventListener('click', () => {
saveBgColor(colorPicker.value);
applyColor.textContent = '✓ ذخیره شد';
setTimeout(() => applyColor.textContent = 'اعمال', 2000);
});
// ═══ عنوان صفحه گردونه ═══
function initTitle() {
const settings = getTitleSettings();
document.getElementById('title-text-input').value = settings.text;
document.getElementById('title-color-picker').value = settings.color;
document.getElementById('title-preview').textContent = settings.text;
document.getElementById('title-preview').style.color = settings.color;
}
document.getElementById('title-text-input').addEventListener('input', (e) => {
document.getElementById('title-preview').textContent = e.target.value || 'گردونه شانس';
});
document.getElementById('title-color-picker').addEventListener('input', (e) => {
document.getElementById('title-preview').style.color = e.target.value;
});
document.getElementById('apply-title').addEventListener('click', () => {
const text = document.getElementById('title-text-input').value.trim() || 'گردونه شانس';
const color = document.getElementById('title-color-picker').value;
saveTitleSettings(text, color);
document.getElementById('apply-title').textContent = '✓ ذخیره شد';
setTimeout(() => document.getElementById('apply-title').textContent = 'اعمال', 2000);
});
// ═══ badge نوع جایزه ═══
function prizeBadge(type) {
const map = {
'پوچ': ['badge-pooch', 'پوچ'],
'کد تخفیف': ['badge-discount', 'کد تخفیف'],
'لایوکوین': ['badge-coin', 'لایوکوین (LC)'],
};
const [cls, label] = map[type] || ['badge-pooch', type];
return `<span class="badge ${cls}">${label}</span>`;
}
// ═══ رندر جدول ═══
async function renderItems() {
const items = await adminGetItems();
tableBody.innerHTML = '';
items.forEach((item, idx) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${idx + 1}</td>
<td>${prizeBadge(item.prize_type)}</td>
<td>${item.title}</td>
<td>${item.prize}</td>
<td>${item.probability}</td>
<td>${item.description || '—'}</td>
<td>
<button class="edit-btn" onclick="openEdit(${item.id})">✏️ ویرایش</button>
<button class="delete-btn" onclick="removeItem(${item.id})">🗑 حذف</button>
</td>
`;
tableBody.appendChild(row);
});
window._items = items;
}
// ═══ افزودن آیتم ═══
addBtn.onclick = async () => {
const data = {
prize_type: document.getElementById('new-prize-type').value,
title: document.getElementById('new-title').value.trim(),
prize: document.getElementById('new-prize').value.trim(),
probability: parseFloat(document.getElementById('new-probability').value),
description: document.getElementById('new-description').value.trim(),
};
if (!data.title || !data.prize) return alert('عنوان و مقدار جایزه رو پر کن');
await createItem(data);
document.getElementById('new-title').value = '';
document.getElementById('new-prize').value = '';
document.getElementById('new-description').value = '';
document.getElementById('new-probability').value = '1';
renderItems();
};
// ═══ حذف ═══
window.removeItem = async (id) => { window.removeItem = async (id) => {
if (!confirm('آیا مطمئنی؟')) return;
await deleteItem(id); await deleteItem(id);
renderItems(); renderItems();
}; };
// -------------------- Edit -------------------- // ═══ ویرایش ═══
window.editItem = (id, title, value) => { window.openEdit = (id) => {
titleInput.value = title; const item = window._items.find(i => i.id === id);
valueInput.value = value; if (!item) return;
document.getElementById('edit-id').value = item.id;
editId = id; document.getElementById('edit-prize-type').value = item.prize_type;
document.getElementById('edit-title').value = item.title;
addBtn.textContent = 'آپدیت ✏️'; document.getElementById('edit-prize').value = item.prize;
document.getElementById('edit-probability').value = item.probability;
document.getElementById('edit-description').value = item.description || '';
editModal.classList.remove('hidden');
}; };
// -------------------- Add / Update -------------------- cancelEdit.onclick = () => editModal.classList.add('hidden');
addBtn.onclick = async () => {
const title = titleInput.value.trim();
const value = valueInput.value.trim();
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 = '';
saveEdit.onclick = async () => {
const id = document.getElementById('edit-id').value;
const data = {
prize_type: document.getElementById('edit-prize-type').value,
title: document.getElementById('edit-title').value.trim(),
prize: document.getElementById('edit-prize').value.trim(),
probability: parseFloat(document.getElementById('edit-probability').value),
description: document.getElementById('edit-description').value.trim(),
};
await updateItem(id, data);
editModal.classList.add('hidden');
renderItems(); renderItems();
}; };
// -------------------- Init -------------------- initColor();
initTitle();
renderItems(); renderItems();

View File

@@ -1,40 +1,74 @@
const BASE = 'http://127.0.0.1:8000'; const BASE = 'http://127.0.0.1:8000';
// ---------------- USER ----------------
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(async (r) => {
method: 'POST' if (!r.ok) throw new Error('Spin failed');
}).then(r => r.json()); return r.json();
});
// ---------------- ADMIN ----------------
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 = (data) =>
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: data.title,
body: JSON.stringify(data) prize: data.prize,
prize_type: data.prize_type,
description: data.description,
probability: data.probability
})
}).then(r => r.json()); }).then(r => r.json());
export const updateItem = (id, data) => export const updateItem = (id, data) =>
fetch(`${BASE}/admin/items/${id}`, { fetch(`${BASE}/admin/items/${id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({
}, title: data.title,
body: JSON.stringify(data) prize: data.prize,
prize_type: data.prize_type,
description: data.description,
probability: data.probability
})
}).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(async (r) => {
method: 'DELETE' if (!r.ok && r.status !== 204) throw new Error('Delete failed');
}).then(r => { return {};
if (r.status === 204) return {};
return r.json();
}); });
// ---------------- رنگ پس‌زمینه ----------------
export const getBgColor = () =>
localStorage.getItem('wheelBgColor') || '#7B241C';
export const saveBgColor = (color) => {
localStorage.setItem('wheelBgColor', color);
window.dispatchEvent(new StorageEvent('storage', {
key: 'wheelBgColor',
newValue: color
}));
};
// ---------------- عنوان صفحه گردونه ----------------
export const getTitleSettings = () => {
const saved = localStorage.getItem('wheelTitleSettings');
return saved ? JSON.parse(saved) : { text: 'گردونه شانس', color: '#FFD700' };
};
export const saveTitleSettings = (text, color) => {
const settings = { text, color };
localStorage.setItem('wheelTitleSettings', JSON.stringify(settings));
window.dispatchEvent(new StorageEvent('storage', {
key: 'wheelTitleSettings',
newValue: JSON.stringify(settings)
}));
};

View File

@@ -1,4 +1,4 @@
import { getItems, spinWheel } from './api.js'; import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
const canvas = document.getElementById('wheel'); const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@@ -8,12 +8,43 @@ const result = document.getElementById('result');
const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' }; const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' };
const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' }; const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' };
let audioCtx = null;
function getAudioCtx() {
if (!audioCtx || audioCtx.state === 'closed') {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
let items = []; let items = [];
let angle = 0; let angle = 0;
let spinning = false; let spinning = false;
let lampOn = true; let lampOn = true;
function applyBg() {
const color = getBgColor();
document.getElementById('main-body').style.background =
`radial-gradient(ellipse at center, ${color}DD 0%, ${color} 55%, ${color}BB 100%)`;
}
function applyTitle() {
const settings = getTitleSettings();
const h1 = document.querySelector('.page h1');
if (h1) {
h1.textContent = settings.text;
h1.style.color = settings.color;
h1.style.textShadow = `0 0 20px ${settings.color}80`;
}
}
async function init() { async function init() {
applyBg();
applyTitle();
window.addEventListener('storage', (e) => {
if (e.key === 'wheelBgColor') applyBg();
if (e.key === 'wheelTitleSettings') applyTitle();
});
items = await getItems(); items = await getItems();
draw(angle); draw(angle);
setInterval(() => { setInterval(() => {
@@ -75,7 +106,6 @@ function draw(rotation) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// ═══ سایه کلی ═══
ctx.save(); ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 30; ctx.shadowBlur = 30;
@@ -87,7 +117,6 @@ function draw(rotation) {
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
// ═══ حلقه بیرونی تیره ═══
const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40); const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40);
rimGrad.addColorStop(0, '#A0003A'); rimGrad.addColorStop(0, '#A0003A');
rimGrad.addColorStop(0.3, '#6D0025'); rimGrad.addColorStop(0.3, '#6D0025');
@@ -98,14 +127,12 @@ function draw(rotation) {
ctx.fillStyle = rimGrad; ctx.fillStyle = rimGrad;
ctx.fill(); 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)); drawSegment(cx, cy, r, start, end, getTheme(i, total));
}); });
// ═══ خطوط طلایی ═══
items.forEach((_, i) => { items.forEach((_, i) => {
const lineAngle = rotation + i * slice; const lineAngle = rotation + i * slice;
const lGrad = ctx.createLinearGradient( const lGrad = ctx.createLinearGradient(
@@ -113,7 +140,7 @@ function draw(rotation) {
cx + r * Math.cos(lineAngle), cx + r * Math.cos(lineAngle),
cy + r * Math.sin(lineAngle) cy + r * Math.sin(lineAngle)
); );
lGrad.addColorStop(0, 'rgba(700,550,0,5)'); lGrad.addColorStop(0, 'rgba(255,215,0,0.3)');
lGrad.addColorStop(0.4, '#a2922e'); lGrad.addColorStop(0.4, '#a2922e');
lGrad.addColorStop(1, '#c8b43b'); lGrad.addColorStop(1, '#c8b43b');
ctx.beginPath(); ctx.beginPath();
@@ -124,31 +151,29 @@ function draw(rotation) {
ctx.stroke(); ctx.stroke();
}); });
// ═══ متن آیتم‌ها ═══
items.forEach((item, i) => { items.forEach((item, i) => {
const start = rotation + i * slice; const start = rotation + i * slice;
const theme = getTheme(i, total); 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 = 'center';
ctx.fillStyle = theme.text; ctx.fillStyle = theme.text;
ctx.font = 'bold 12px Tahoma'; ctx.font = 'bold 18px Dana';
ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 4; ctx.shadowBlur = 4;
const text = item.title; const text = item.title;
const maxWidth = r - 52; const maxWidth = r - 52;
if (ctx.measureText(text).width > maxWidth) { if (ctx.measureText(text).width > maxWidth) {
const mid = Math.ceil(text.length / 2); const mid = Math.ceil(text.length / 2);
ctx.fillText(text.slice(0, mid), r - 20, -7); ctx.fillText(text.slice(0, mid), (r - 20) / 1.4, -9);
ctx.fillText(text.slice(mid), r - 20, 9); ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9);
} else { } else {
ctx.fillText(text, r - 20, 5); ctx.fillText(text, (r - 20) / 1.4, 5);
} }
ctx.restore(); ctx.restore();
}); });
// ═══ حلقه طلایی داخلی ═══
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, r + 2, 0, Math.PI * 2); ctx.arc(cx, cy, r + 2, 0, Math.PI * 2);
ctx.strokeStyle = '#dfd498'; ctx.strokeStyle = '#dfd498';
@@ -158,7 +183,6 @@ function draw(rotation) {
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// ═══ لامپ‌ها ═══
const lampCount = 24; const lampCount = 24;
const lampR = r + 23; const lampR = r + 23;
for (let i = 0; i < lampCount; i++) { for (let i = 0; i < lampCount; i++) {
@@ -208,7 +232,6 @@ function draw(rotation) {
ctx.fill(); ctx.fill();
} }
// ═══ حلقه طلایی بیرونی ═══
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2); ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
ctx.strokeStyle = '#C8960C'; ctx.strokeStyle = '#C8960C';
@@ -218,7 +241,6 @@ function draw(rotation) {
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// ═══ دایره مرکزی — هماهنگ با نشانگر ═══
const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22); const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22);
cGrad.addColorStop(0, '#FFFFF0'); cGrad.addColorStop(0, '#FFFFF0');
cGrad.addColorStop(0.15, '#FFE566'); cGrad.addColorStop(0.15, '#FFE566');
@@ -240,18 +262,15 @@ function draw(rotation) {
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
// بازتاب روی دایره مرکزی
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2); ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fill(); ctx.fill();
// ═══ نشانگر ═══
drawPointer(cx); drawPointer(cx);
} }
function drawPointer(cx) { function drawPointer(cx) {
// سایه
ctx.save(); ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.4)'; ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 8; ctx.shadowBlur = 8;
@@ -263,7 +282,6 @@ function drawPointer(cx) {
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
// دایره طلایی — همرنگ مرکز
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18); const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
pGrad.addColorStop(0, '#FFFFF0'); pGrad.addColorStop(0, '#FFFFF0');
pGrad.addColorStop(0.15, '#FFE566'); pGrad.addColorStop(0.15, '#FFE566');
@@ -282,13 +300,11 @@ function drawPointer(cx) {
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
// بازتاب
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2); ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fill(); ctx.fill();
// مثلث — همرنگ
const triGrad = ctx.createLinearGradient(cx, 34, cx, 62); const triGrad = ctx.createLinearGradient(cx, 34, cx, 62);
triGrad.addColorStop(0, '#FFE566'); triGrad.addColorStop(0, '#FFE566');
triGrad.addColorStop(0.35, '#FFD700'); triGrad.addColorStop(0.35, '#FFD700');
@@ -309,6 +325,52 @@ function drawPointer(cx) {
ctx.stroke(); ctx.stroke();
} }
function playTickSound() {
try {
const actx = getAudioCtx();
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = 600;
osc.type = 'sine';
gain.gain.setValueAtTime(0.15, actx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.08);
osc.start(actx.currentTime);
osc.stop(actx.currentTime + 0.08);
} catch(e) {}
}
function playWinSound() {
try {
const actx = getAudioCtx();
const notes = [523, 659, 784, 1047];
notes.forEach((freq, i) => {
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = freq;
osc.type = 'sine';
gain.gain.setValueAtTime(0.2, actx.currentTime + i * 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + i * 0.15 + 0.3);
osc.start(actx.currentTime + i * 0.15);
osc.stop(actx.currentTime + i * 0.15 + 0.3);
});
} catch(e) {}
}
function showWinnerModal(winner) {
const modal = document.getElementById('winner-modal');
const iconMap = { 'پوچ': '', 'کد تخفیف': '', 'لایوکوین': '' };
document.getElementById('modal-icon').textContent = iconMap[winner.prize_type] || '🎉';
document.getElementById('modal-prize-type').textContent = winner.prize_type;
document.getElementById('modal-prize').textContent = winner.prize;
document.getElementById('modal-description').textContent = winner.description || '';
modal.classList.remove('hidden');
document.getElementById('modal-close').onclick = () => modal.classList.add('hidden');
}
btn.onclick = async () => { btn.onclick = async () => {
if (spinning || items.length === 0) return; if (spinning || items.length === 0) return;
spinning = true; spinning = true;
@@ -328,12 +390,20 @@ btn.onclick = async () => {
const duration = 5500; const duration = 5500;
const startTime = performance.now(); const startTime = performance.now();
const from = angle; const from = angle;
let lastTickIdx = 0;
function animate(now) { function animate(now) {
const elapsed = now - startTime; 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, 4); const ease = 1 - Math.pow(1 - t, 4);
angle = from + (targetAngle - from) * ease; angle = from + (targetAngle - from) * ease;
const currentIdx = Math.floor(Math.abs(angle) / slice);
if (currentIdx !== lastTickIdx) {
lastTickIdx = currentIdx;
playTickSound();
}
draw(angle); draw(angle);
if (t < 1) { if (t < 1) {
@@ -342,7 +412,8 @@ btn.onclick = async () => {
spinning = false; spinning = false;
btn.disabled = false; btn.disabled = false;
draw(angle); draw(angle);
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`; playWinSound();
showWinnerModal(winner);
} }
} }

View File

@@ -2,18 +2,34 @@
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>گردونه شانس</title> <title>گردونه شانس</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body> <body id="main-body">
<div class="bg-rays"></div>
<div class="page"> <div class="page">
<h1>گردونه شانس</h1> <h1> گردونه شانس</h1>
<div class="wheel-wrapper"> <div class="wheel-wrapper">
<canvas id="wheel" width="500" height="500"></canvas> <canvas id="wheel" width="500" height="500"></canvas>
</div> </div>
<button id="spin-btn">بچرخون!</button> <button id="spin-btn">چرخوندن!</button>
<div id="result"></div> <div id="result"></div>
</div> </div>
<!-- مودال برنده -->
<div id="winner-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-icon" id="modal-icon">🎉</div>
<div id="modal-prize-type"></div>
<div id="modal-prize"></div>
<div id="modal-description"></div>
<button id="modal-close">باشه!</button>
</div>
</div>
<script type="module" src="assets/js/wheel.js"></script> <script type="module" src="assets/js/wheel.js"></script>
</body> </body>
</html> </html>