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
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
backend/alembic/versions/eb35214a1411_update_items_table.py
Normal file
28
backend/alembic/versions/eb35214a1411_update_items_table.py
Normal 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
|
||||
28
backend/alembic/versions/f71dc2bbdfc6_update_items_table.py
Normal file
28
backend/alembic/versions/f71dc2bbdfc6_update_items_table.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import schemas
|
||||
from schemas import ItemResponse
|
||||
from services import crud
|
||||
@@ -13,7 +9,6 @@ from core.logger import get_logger
|
||||
router = APIRouter()
|
||||
logger = get_logger('admin')
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -21,7 +16,6 @@ def get_db():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# GET ALL ITEMS
|
||||
@router.get("/items", response_model=list[ItemResponse])
|
||||
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}")
|
||||
raise
|
||||
|
||||
|
||||
# CREATE ITEM
|
||||
@router.post("/items", response_model=ItemResponse)
|
||||
def create_item(
|
||||
item: schemas.ItemCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
|
||||
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}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.critical(f"Unexpected error creating item: {e}")
|
||||
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
|
||||
@router.delete("/items/{item_id}")
|
||||
def delete_item(
|
||||
item_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
||||
try:
|
||||
item = crud.delete_item(db, item_id)
|
||||
if not item:
|
||||
@@ -66,28 +83,4 @@ def delete_item(
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.critical(f"Unexpected error deleting item: {e}")
|
||||
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
|
||||
|
||||
|
||||
raise
|
||||
@@ -51,7 +51,10 @@ def spin(db: Session = Depends(get_db)):
|
||||
return {
|
||||
"winner": {
|
||||
"id": winner.id,
|
||||
"title": winner.title
|
||||
"title": winner.title,
|
||||
"prize": winner.prize,
|
||||
"prize_type": winner.prize_type,
|
||||
"description": winner.description or ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,26 +1,12 @@
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Float
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Item(Base):
|
||||
|
||||
__tablename__ = "items"
|
||||
|
||||
id = Column(
|
||||
Integer,
|
||||
primary_key=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
title = Column(
|
||||
String,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
value = Column(
|
||||
Integer,
|
||||
nullable=False
|
||||
)
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
prize = Column(String, nullable=False)
|
||||
description = Column(String, nullable=True, default="")
|
||||
prize_type = Column(String, nullable=False, default="پوچ")
|
||||
probability = Column(Float, nullable=False, default=1.0)
|
||||
@@ -287,3 +287,137 @@ FROM 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: پوچ
|
||||
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
|
||||
|
||||
@@ -38,3 +38,35 @@ FROM items]
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(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)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.db.session import engine
|
||||
from app.db.session import Base
|
||||
|
||||
from app.db.session import engine, Base
|
||||
from app.db.models import Item
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
from api.admin import router as admin_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(
|
||||
CORSMiddleware,
|
||||
@@ -26,4 +21,8 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(admin_router, prefix="/admin")
|
||||
# ✅ بعد router ها
|
||||
app.include_router(user_router, prefix="/user")
|
||||
app.include_router(admin_router, prefix="/admin")
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -1,20 +1,27 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from typing import Optional
|
||||
|
||||
class ItemCreate(BaseModel):
|
||||
title: str
|
||||
value: int
|
||||
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str = "پوچ"
|
||||
probability: float = 1.0
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
title: str
|
||||
value: int
|
||||
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str = "پوچ"
|
||||
probability: float = 1.0
|
||||
|
||||
class ItemResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
value: int
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str
|
||||
probability: float
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Binary file not shown.
@@ -5,18 +5,27 @@ from app.db.models import Item
|
||||
def get_items(db: Session):
|
||||
return db.query(Item).all()
|
||||
|
||||
def create_item(db: Session, title: str, value: int): # ✅ prize → value
|
||||
item = Item(title=title, value=value) # ✅ prize → value
|
||||
def create_item(db: Session, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
|
||||
item = Item(
|
||||
title=title,
|
||||
prize=prize,
|
||||
description=description,
|
||||
prize_type=prize_type,
|
||||
probability=probability
|
||||
)
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(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()
|
||||
if item:
|
||||
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.refresh(item)
|
||||
return item
|
||||
@@ -32,4 +41,5 @@ def spin_wheel(db: Session):
|
||||
items = db.query(Item).all()
|
||||
if not items:
|
||||
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.
@@ -1,92 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>پنل مدیریت</title>
|
||||
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<body id="main-body">
|
||||
<div class="admin-container">
|
||||
|
||||
<div class="admin-header">
|
||||
<h1> مدیریت گردونه شانس</h1>
|
||||
<h1>مدیریت گردونه شانس</h1>
|
||||
<p>مدیریت آیتمها و جوایز گردونه</p>
|
||||
</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">
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="title-input"
|
||||
placeholder="عنوان جایزه"
|
||||
>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
id="value-input"
|
||||
placeholder="مقدار جایزه"
|
||||
>
|
||||
|
||||
<button id="add-btn">
|
||||
افزودن
|
||||
</button>
|
||||
|
||||
<div class="card">
|
||||
<h2>افزودن آیتم جدید</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>نوع جایزه:</label>
|
||||
<select id="new-prize-type">
|
||||
<option value="پوچ">پوچ</option>
|
||||
<option value="کد تخفیف">کد تخفیف</option>
|
||||
<option value="لایوکوین">لایوکوین (LC)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>عنوان:</label>
|
||||
<input type="text" id="new-title" placeholder="مثلاً: تخفیف ۲۰٪">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 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 class="card">
|
||||
<h2>لیست آیتمها</h2>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>نوع جایزه</th>
|
||||
<th>عنوان</th>
|
||||
<th>مقدار</th>
|
||||
<th>احتمال</th>
|
||||
<th>توضیحات</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-table-body"></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>
|
||||
|
||||
<script type="module" src="assets/js/admin.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,261 +1,68 @@
|
||||
/* =========================
|
||||
ADMIN PANEL
|
||||
========================= */
|
||||
|
||||
.admin-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding: 30px;
|
||||
@font-face {
|
||||
font-family: 'Dana';
|
||||
src: url('../fonts/DanaFaNum-Regular.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Dana' !important;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
WHEEL PAGE
|
||||
========================= */
|
||||
body {
|
||||
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;
|
||||
justify-content: 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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
gap: 28px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.page h1 {
|
||||
color: #FFD700;
|
||||
font-size: 36px;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||
color: #e9e3c0;
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 20px rgba(255,215,0,0.5);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.wheel-wrapper {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
.wheel-wrapper canvas {
|
||||
@@ -265,22 +72,21 @@ body {
|
||||
}
|
||||
|
||||
#spin-btn {
|
||||
padding: 16px 48px;
|
||||
font-size: 20px;
|
||||
padding: 18px 56px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-family: Tahoma, sans-serif;
|
||||
background: linear-gradient(135deg, #FFD700, #D4AC0D);
|
||||
background: linear-gradient(135deg, #e7d78a, #f2dd9d);
|
||||
color: #4a0f0f;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
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;
|
||||
}
|
||||
|
||||
#spin-btn:hover {
|
||||
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 {
|
||||
@@ -295,10 +101,10 @@ body {
|
||||
}
|
||||
|
||||
.winner-text {
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -308,18 +114,380 @@ body {
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* =========================
|
||||
مودال برنده
|
||||
========================= */
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.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; }
|
||||
}
|
||||
|
||||
.page h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
.title-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
#spin-btn {
|
||||
font-size: 16px;
|
||||
padding: 12px 36px;
|
||||
}
|
||||
.title-section h3 {
|
||||
color: #f5d6a0;
|
||||
font-size: 15px;
|
||||
margin-bottom: 14px;
|
||||
font-family: 'Dana', Tahoma, sans-serif;
|
||||
}
|
||||
BIN
frontend/assets/fonts/DanaFaNum-Regular.ttf
Normal file
BIN
frontend/assets/fonts/DanaFaNum-Regular.ttf
Normal file
Binary file not shown.
@@ -1,103 +1,158 @@
|
||||
import {
|
||||
adminGetItems,
|
||||
createItem,
|
||||
deleteItem,
|
||||
updateItem // 👈 باید اینو هم به api.js اضافه کنی
|
||||
} from './api.js';
|
||||
import { adminGetItems, createItem, deleteItem, updateItem, saveBgColor, getBgColor, saveTitleSettings, getTitleSettings } from './api.js';
|
||||
|
||||
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 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; // 👈 حالت ویرایش
|
||||
|
||||
// -------------------- Render --------------------
|
||||
async function renderItems() {
|
||||
const items = await adminGetItems();
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
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);
|
||||
// ═══ رنگ پسزمینه ═══
|
||||
function initColor() {
|
||||
const saved = getBgColor();
|
||||
colorPicker.value = saved;
|
||||
previewBox.style.background = saved;
|
||||
presets.forEach(p => {
|
||||
if (p.dataset.color === saved) p.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------- 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) => {
|
||||
if (!confirm('آیا مطمئنی؟')) return;
|
||||
await deleteItem(id);
|
||||
renderItems();
|
||||
};
|
||||
|
||||
// -------------------- Edit --------------------
|
||||
window.editItem = (id, title, value) => {
|
||||
titleInput.value = title;
|
||||
valueInput.value = value;
|
||||
|
||||
editId = id;
|
||||
|
||||
addBtn.textContent = 'آپدیت ✏️';
|
||||
// ═══ ویرایش ═══
|
||||
window.openEdit = (id) => {
|
||||
const item = window._items.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
document.getElementById('edit-id').value = item.id;
|
||||
document.getElementById('edit-prize-type').value = item.prize_type;
|
||||
document.getElementById('edit-title').value = item.title;
|
||||
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 --------------------
|
||||
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 = '';
|
||||
cancelEdit.onclick = () => editModal.classList.add('hidden');
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// -------------------- Init --------------------
|
||||
initColor();
|
||||
initTitle();
|
||||
renderItems();
|
||||
@@ -1,40 +1,74 @@
|
||||
const BASE = 'http://127.0.0.1:8000';
|
||||
|
||||
// ---------------- USER ----------------
|
||||
export const getItems = () =>
|
||||
fetch(`${BASE}/user/items`)
|
||||
.then(r => r.json());
|
||||
fetch(`${BASE}/user/items`).then(r => r.json());
|
||||
|
||||
export const spinWheel = () =>
|
||||
fetch(`${BASE}/user/spin`, {
|
||||
method: 'POST'
|
||||
}).then(r => r.json());
|
||||
fetch(`${BASE}/user/spin`, { method: 'POST' }).then(async (r) => {
|
||||
if (!r.ok) throw new Error('Spin failed');
|
||||
return r.json();
|
||||
});
|
||||
|
||||
// ---------------- ADMIN ----------------
|
||||
export const adminGetItems = () =>
|
||||
fetch(`${BASE}/admin/items`)
|
||||
.then(r => r.json());
|
||||
fetch(`${BASE}/admin/items`).then(r => r.json());
|
||||
|
||||
export const createItem = (data) =>
|
||||
fetch(`${BASE}/admin/items`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: data.title,
|
||||
prize: data.prize,
|
||||
prize_type: data.prize_type,
|
||||
description: data.description,
|
||||
probability: data.probability
|
||||
})
|
||||
}).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)
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: data.title,
|
||||
prize: data.prize,
|
||||
prize_type: data.prize_type,
|
||||
description: data.description,
|
||||
probability: data.probability
|
||||
})
|
||||
}).then(r => r.json());
|
||||
|
||||
export const deleteItem = (id) =>
|
||||
fetch(`${BASE}/admin/items/${id}`, {
|
||||
method: 'DELETE'
|
||||
}).then(r => {
|
||||
if (r.status === 204) return {};
|
||||
return r.json();
|
||||
});
|
||||
fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(async (r) => {
|
||||
if (!r.ok && r.status !== 204) throw new Error('Delete failed');
|
||||
return {};
|
||||
});
|
||||
|
||||
// ---------------- رنگ پسزمینه ----------------
|
||||
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)
|
||||
}));
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getItems, spinWheel } from './api.js';
|
||||
import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
|
||||
|
||||
const canvas = document.getElementById('wheel');
|
||||
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 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 angle = 0;
|
||||
let spinning = false;
|
||||
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() {
|
||||
applyBg();
|
||||
applyTitle();
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'wheelBgColor') applyBg();
|
||||
if (e.key === 'wheelTitleSettings') applyTitle();
|
||||
});
|
||||
items = await getItems();
|
||||
draw(angle);
|
||||
setInterval(() => {
|
||||
@@ -75,7 +106,6 @@ function draw(rotation) {
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// ═══ سایه کلی ═══
|
||||
ctx.save();
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 30;
|
||||
@@ -87,7 +117,6 @@ function draw(rotation) {
|
||||
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');
|
||||
@@ -98,14 +127,12 @@ function draw(rotation) {
|
||||
ctx.fillStyle = rimGrad;
|
||||
ctx.fill();
|
||||
|
||||
// ═══ بخشهای گردونه ═══
|
||||
items.forEach((item, i) => {
|
||||
const start = rotation + i * 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(
|
||||
@@ -113,7 +140,7 @@ function draw(rotation) {
|
||||
cx + r * Math.cos(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(1, '#c8b43b');
|
||||
ctx.beginPath();
|
||||
@@ -124,31 +151,29 @@ function draw(rotation) {
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// ═══ متن آیتمها ═══
|
||||
items.forEach((item, i) => {
|
||||
const start = rotation + i * slice;
|
||||
const theme = getTheme(i, total);
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
ctx.rotate(start + slice / 2);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = theme.text;
|
||||
ctx.font = 'bold 12px Tahoma';
|
||||
ctx.font = 'bold 18px Dana';
|
||||
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);
|
||||
ctx.fillText(text.slice(0, mid), (r - 20) / 1.4, -9);
|
||||
ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9);
|
||||
} else {
|
||||
ctx.fillText(text, r - 20, 5);
|
||||
ctx.fillText(text, (r - 20) / 1.4, 5);
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// ═══ حلقه طلایی داخلی ═══
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#dfd498';
|
||||
@@ -158,7 +183,6 @@ function draw(rotation) {
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// ═══ لامپها ═══
|
||||
const lampCount = 24;
|
||||
const lampR = r + 23;
|
||||
for (let i = 0; i < lampCount; i++) {
|
||||
@@ -208,7 +232,6 @@ function draw(rotation) {
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ═══ حلقه طلایی بیرونی ═══
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = '#C8960C';
|
||||
@@ -218,7 +241,6 @@ function draw(rotation) {
|
||||
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');
|
||||
@@ -240,18 +262,15 @@ function draw(rotation) {
|
||||
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;
|
||||
@@ -263,7 +282,6 @@ function drawPointer(cx) {
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// دایره طلایی — همرنگ مرکز
|
||||
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
|
||||
pGrad.addColorStop(0, '#FFFFF0');
|
||||
pGrad.addColorStop(0.15, '#FFE566');
|
||||
@@ -282,13 +300,11 @@ function drawPointer(cx) {
|
||||
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');
|
||||
@@ -309,6 +325,52 @@ function drawPointer(cx) {
|
||||
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 () => {
|
||||
if (spinning || items.length === 0) return;
|
||||
spinning = true;
|
||||
@@ -328,12 +390,20 @@ btn.onclick = async () => {
|
||||
const duration = 5500;
|
||||
const startTime = performance.now();
|
||||
const from = angle;
|
||||
let lastTickIdx = 0;
|
||||
|
||||
function animate(now) {
|
||||
const elapsed = now - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 4);
|
||||
angle = from + (targetAngle - from) * ease;
|
||||
|
||||
const currentIdx = Math.floor(Math.abs(angle) / slice);
|
||||
if (currentIdx !== lastTickIdx) {
|
||||
lastTickIdx = currentIdx;
|
||||
playTickSound();
|
||||
}
|
||||
|
||||
draw(angle);
|
||||
|
||||
if (t < 1) {
|
||||
@@ -342,7 +412,8 @@ btn.onclick = async () => {
|
||||
spinning = false;
|
||||
btn.disabled = false;
|
||||
draw(angle);
|
||||
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
|
||||
playWinSound();
|
||||
showWinnerModal(winner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,34 @@
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>گردونه شانس</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<body id="main-body">
|
||||
<div class="bg-rays"></div>
|
||||
<div class="page">
|
||||
<h1>گردونه شانس</h1>
|
||||
<h1> گردونه شانس</h1>
|
||||
<div class="wheel-wrapper">
|
||||
<canvas id="wheel" width="500" height="500"></canvas>
|
||||
</div>
|
||||
<button id="spin-btn">بچرخون!</button>
|
||||
<button id="spin-btn">چرخوندن!</button>
|
||||
<div id="result"></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>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user