diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 8048de5..1504b45 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/__pycache__/schemas.cpython-313.pyc b/backend/__pycache__/schemas.cpython-313.pyc index b605276..55a90f2 100644 Binary files a/backend/__pycache__/schemas.cpython-313.pyc and b/backend/__pycache__/schemas.cpython-313.pyc differ diff --git a/backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc b/backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc new file mode 100644 index 0000000..ab06778 Binary files /dev/null and b/backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc differ diff --git a/backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc b/backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc new file mode 100644 index 0000000..2593d94 Binary files /dev/null and b/backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc differ diff --git a/backend/alembic/versions/eb35214a1411_update_items_table.py b/backend/alembic/versions/eb35214a1411_update_items_table.py new file mode 100644 index 0000000..29c1da5 --- /dev/null +++ b/backend/alembic/versions/eb35214a1411_update_items_table.py @@ -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 diff --git a/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py b/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py new file mode 100644 index 0000000..490762c --- /dev/null +++ b/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py @@ -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 diff --git a/backend/api/__pycache__/admin.cpython-313.pyc b/backend/api/__pycache__/admin.cpython-313.pyc index d8926f4..04fb49d 100644 Binary files a/backend/api/__pycache__/admin.cpython-313.pyc and b/backend/api/__pycache__/admin.cpython-313.pyc differ diff --git a/backend/api/__pycache__/user.cpython-313.pyc b/backend/api/__pycache__/user.cpython-313.pyc index 51e0d47..3747bc8 100644 Binary files a/backend/api/__pycache__/user.cpython-313.pyc and b/backend/api/__pycache__/user.cpython-313.pyc differ diff --git a/backend/api/admin.py b/backend/api/admin.py index 2d76f01..06c23cf 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -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 - - \ No newline at end of file + raise \ No newline at end of file diff --git a/backend/api/user.py b/backend/api/user.py index e8e0fb9..d8b3a45 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -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 "" } } diff --git a/backend/app/db/__pycache__/models.cpython-313.pyc b/backend/app/db/__pycache__/models.cpython-313.pyc index 36d6023..2b26073 100644 Binary files a/backend/app/db/__pycache__/models.cpython-313.pyc and b/backend/app/db/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 48d048c..b9c0c4d 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -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 - ) \ No newline at end of file + 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) \ No newline at end of file diff --git a/backend/logs/app.log b/backend/logs/app.log index c3509ca..c77bd98 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -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 diff --git a/backend/logs/error.log b/backend/logs/error.log index 9d08148..895e7b1 100644 --- a/backend/logs/error.log +++ b/backend/logs/error.log @@ -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) diff --git a/backend/main.py b/backend/main.py index fa62976..113a07c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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") \ No newline at end of file +# ✅ بعد router ها +app.include_router(user_router, prefix="/user") +app.include_router(admin_router, prefix="/admin") + +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/backend/schemas.py b/backend/schemas.py index 45c04a2..ef9e1f3 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -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 \ No newline at end of file diff --git a/backend/services/__pycache__/crud.cpython-313.pyc b/backend/services/__pycache__/crud.cpython-313.pyc index 3c9f161..fa52f42 100644 Binary files a/backend/services/__pycache__/crud.cpython-313.pyc and b/backend/services/__pycache__/crud.cpython-313.pyc differ diff --git a/backend/services/crud.py b/backend/services/crud.py index c17828a..c83354a 100644 --- a/backend/services/crud.py +++ b/backend/services/crud.py @@ -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) \ No newline at end of file + weights = [item.probability for item in items] + return random.choices(items, weights=weights, k=1)[0] \ No newline at end of file diff --git a/backend/spinwheel.db b/backend/spinwheel.db index f2d2d47..492947f 100644 Binary files a/backend/spinwheel.db and b/backend/spinwheel.db differ diff --git a/frontend/admin.html b/frontend/admin.html index 462e254..598d619 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1,92 +1,161 @@ - - - پنل مدیریت - + - - - +
-
-

مدیریت گردونه شانس

+

مدیریت گردونه شانس

مدیریت آیتم‌ها و جوایز گردونه

+ +
+

تنظیمات ظاهری

+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+ پیش‌نمایش: +
+
+
+
+ + +
+

عنوان صفحه گردونه

+
+
+ + +
+
+ +
+ + گردونه شانس +
+
+
+ +
+
+
+ -
- - - - - - - +
+

افزودن آیتم جدید

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
-
- - - - - - - - - - - - - - - - - - - -
#عنوان جایزهمقدارعملیات
- +
+

لیست آیتم‌ها

+
+ + + + + + + + + + + + + +
#نوع جایزهعنوانمقداراحتمالتوضیحاتعملیات
+
+
+ + - - - \ No newline at end of file + diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 5f262d2..301ef9b 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -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; } \ No newline at end of file diff --git a/frontend/assets/fonts/DanaFaNum-Regular.ttf b/frontend/assets/fonts/DanaFaNum-Regular.ttf new file mode 100644 index 0000000..080fed2 Binary files /dev/null and b/frontend/assets/fonts/DanaFaNum-Regular.ttf differ diff --git a/frontend/assets/js/admin.js b/frontend/assets/js/admin.js index c81efca..913c24f 100644 --- a/frontend/assets/js/admin.js +++ b/frontend/assets/js/admin.js @@ -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 = ` - ${index + 1} - ${item.title} - ${item.value} - - - - - - - - `; - - 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 `${label}`; +} + +// ═══ رندر جدول ═══ +async function renderItems() { + const items = await adminGetItems(); + tableBody.innerHTML = ''; + items.forEach((item, idx) => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${idx + 1} + ${prizeBadge(item.prize_type)} + ${item.title} + ${item.prize} + ${item.probability} + ${item.description || '—'} + + + + + `; + 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(); \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js index 17958bc..327f1d6 100644 --- a/frontend/assets/js/api.js +++ b/frontend/assets/js/api.js @@ -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(); - }); \ No newline at end of file + 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) + })); +}; \ No newline at end of file diff --git a/frontend/assets/js/wheel.js b/frontend/assets/js/wheel.js index e4ca45c..5185b5f 100644 --- a/frontend/assets/js/wheel.js +++ b/frontend/assets/js/wheel.js @@ -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 = `🎉 ${winner.title} 🎉`; + playWinSound(); + showWinnerModal(winner); } } diff --git a/frontend/index.html b/frontend/index.html index a2039d2..7bd231d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,18 +2,34 @@ + گردونه شانس + - + +
-

گردونه شانس

+

گردونه شانس

- +
+ + + + - \ No newline at end of file +