diff --git a/backend/.env b/backend/.env index 7841a24..e8403d6 100644 --- a/backend/.env +++ b/backend/.env @@ -1,4 +1,3 @@ -DATABASE_URL=sqlite:///./wheel.db - +DATABASE_URL=sqlite:///./spinwheel.db LOG_ENABLED=true LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/backend/__pycache__/config.cpython-313.pyc b/backend/__pycache__/config.cpython-313.pyc index 3a0cad9..ac76f95 100644 Binary files a/backend/__pycache__/config.cpython-313.pyc and b/backend/__pycache__/config.cpython-313.pyc differ diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 5524bfe..8048de5 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 069f2ab..b605276 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__/b55808a86947_add_prize_column.cpython-313.pyc b/backend/alembic/versions/__pycache__/b55808a86947_add_prize_column.cpython-313.pyc new file mode 100644 index 0000000..4f01da5 Binary files /dev/null and b/backend/alembic/versions/__pycache__/b55808a86947_add_prize_column.cpython-313.pyc differ diff --git a/backend/alembic/versions/b55808a86947_add_prize_column.py b/backend/alembic/versions/b55808a86947_add_prize_column.py new file mode 100644 index 0000000..623134a --- /dev/null +++ b/backend/alembic/versions/b55808a86947_add_prize_column.py @@ -0,0 +1,28 @@ +"""add prize column + +Revision ID: b55808a86947 +Revises: ab09ab5070f7 +Create Date: 2026-05-29 19:32:41.615653 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b55808a86947' +down_revision: Union[str, Sequence[str], None] = 'ab09ab5070f7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/backend/api/__pycache__/admin.cpython-313.pyc b/backend/api/__pycache__/admin.cpython-313.pyc index 80a2500..d8926f4 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 891e14f..51e0d47 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 1677316..2d76f01 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -30,7 +30,7 @@ def get_items(db: Session = Depends(get_db)): logger.info(f"Items fetched: {len(items)} items") return items except Exception as e: - logger.error(f"Error fetching items: {e}") + logger.critical(f"Unexpected error fetching items: {e}") raise @@ -41,11 +41,11 @@ def create_item( db: Session = Depends(get_db) ): try: - result = crud.create_item(db, item.title) + result = crud.create_item(db, item.title, item.value) # ✅ value اضافه شد logger.info(f"Item created: {item.title}") return result except Exception as e: - logger.error(f"Error creating item: {e}") + logger.critical(f"Unexpected error creating item: {e}") raise @@ -65,5 +65,29 @@ def delete_item( except HTTPException: raise except Exception as e: - logger.error(f"Error deleting item: {e}") - raise \ No newline at end of file + 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 diff --git a/backend/api/user.py b/backend/api/user.py index a93d67b..e8e0fb9 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -29,7 +29,7 @@ def get_items(db: Session = Depends(get_db)): logger.info(f"User fetched items: {len(items)} items") return items except Exception as e: - logger.error(f"Error fetching items: {e}") + logger.critical(f"Unexpected error fetching items: {e}") raise @@ -38,4 +38,26 @@ def get_items(db: Session = Depends(get_db)): def spin(db: Session = Depends(get_db)): try: winner = crud.spin_wheel(db) - if not winner: \ No newline at end of file + + if not winner: + logger.warning("Spin attempted but no items available") + raise HTTPException( + status_code=400, + detail="No items available" + ) + + logger.info(f"Spin result: {winner.title}") + + return { + "winner": { + "id": winner.id, + "title": winner.title + } + } + + except HTTPException: + raise + + except Exception as e: + logger.critical(f"Unexpected error during spin: {e}") + raise \ No newline at end of file diff --git a/backend/app/db/__pycache__/models.cpython-313.pyc b/backend/app/db/__pycache__/models.cpython-313.pyc index 4c2cd42..36d6023 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 8c90d5b..48d048c 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -18,4 +18,9 @@ class Item(Base): title = Column( String, nullable=False + ) + + value = Column( + Integer, + nullable=False ) \ No newline at end of file diff --git a/backend/config.py b/backend/config.py index 67d7bac..363adcc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,10 +1,16 @@ +from pydantic import Field from pydantic_settings import BaseSettings -class Settings(BaseSettings): - DATABASE_URL: str +class Settings(BaseSettings): + DATABASE_URL: str = Field(..., alias="DATABASE_URL") + + LOG_ENABLED: bool = Field(True, alias="LOG_ENABLED") + LOG_LEVEL: str = Field("INFO", alias="LOG_LEVEL") class Config: env_file = ".env" + populate_by_name = True + settings = Settings() \ No newline at end of file diff --git a/backend/core/__pycache__/logger.cpython-313.pyc b/backend/core/__pycache__/logger.cpython-313.pyc index 92185cb..3bfba40 100644 Binary files a/backend/core/__pycache__/logger.cpython-313.pyc and b/backend/core/__pycache__/logger.cpython-313.pyc differ diff --git a/backend/logs/app.log b/backend/logs/app.log index 78e4cd8..384ad20 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -1 +1,206 @@ 2026-05-24 09:52:10 | INFO | test | test +2026-05-24 12:00:16 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:43 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:47 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:57 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:59 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:01:05 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:42 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:49 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:51 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:12:18 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:24:46 | INFO | admin | Items fetched: 0 items +2026-05-24 12:24:55 | INFO | admin | Item created: جایزه 1 +2026-05-24 12:24:55 | INFO | admin | Items fetched: 1 items +2026-05-24 12:25:07 | INFO | admin | Item created: جایزه 2 +2026-05-24 12:25:07 | INFO | admin | Items fetched: 2 items +2026-05-24 12:25:12 | INFO | admin | Item created: جایزه 3 +2026-05-24 12:25:12 | INFO | admin | Items fetched: 3 items +2026-05-24 12:25:17 | INFO | user | User fetched items: 3 items +2026-05-24 12:25:19 | INFO | user | Spin result: جایزه 3 +2026-05-24 12:26:33 | INFO | admin | Item created: پوچ +2026-05-24 12:26:33 | INFO | admin | Items fetched: 4 items +2026-05-24 12:26:38 | INFO | user | User fetched items: 4 items +2026-05-24 12:26:40 | INFO | user | Spin result: جایزه 1 +2026-05-24 12:41:01 | INFO | user | User fetched items: 4 items +2026-05-24 12:41:02 | INFO | user | User fetched items: 4 items +2026-05-24 12:41:04 | INFO | user | Spin result: پوچ +2026-05-24 12:45:29 | INFO | user | Spin result: پوچ +2026-05-24 12:49:58 | INFO | user | Spin result: جایزه 1 +2026-05-24 12:50:31 | INFO | user | Spin result: پوچ +2026-05-24 12:50:59 | INFO | user | Spin result: جایزه 2 +2026-05-24 12:51:10 | INFO | user | Spin result: جایزه 3 +2026-05-24 12:57:40 | INFO | admin | Item deleted: 4 +2026-05-24 12:57:40 | INFO | admin | Items fetched: 3 items +2026-05-24 12:57:48 | INFO | admin | Item created: پوچ +2026-05-24 12:57:48 | INFO | admin | Items fetched: 4 items +2026-05-24 12:58:01 | INFO | admin | Item created: جایزه 3 +2026-05-24 12:58:01 | INFO | admin | Items fetched: 5 items +2026-05-24 12:58:49 | INFO | user | User fetched items: 5 items +2026-05-29 19:43:29 | INFO | admin | Items fetched: 5 items +2026-05-29 19:43:34 | INFO | admin | Items fetched: 5 items +2026-05-29 19:43:35 | INFO | admin | Items fetched: 5 items +2026-05-29 19:44:03 | INFO | admin | Items fetched: 5 items +2026-05-29 19:44:05 | INFO | admin | Items fetched: 5 items +2026-05-29 19:44:28 | INFO | user | User fetched items: 5 items +2026-05-29 19:45:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:45:18 | INFO | admin | Items fetched: 5 items +2026-05-29 19:45:19 | INFO | admin | Items fetched: 5 items +2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items +2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items +2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items +2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items +2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items +2026-05-29 19:47:15 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:18 | INFO | admin | Items fetched: 5 items +2026-05-29 19:47:20 | INFO | user | User fetched items: 5 items +2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items +2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items +2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items +2026-05-29 19:48:16 | INFO | admin | Items fetched: 5 items +2026-05-29 19:49:38 | INFO | user | User fetched items: 5 items +2026-05-29 19:49:45 | INFO | admin | Items fetched: 5 items +2026-05-29 19:53:58 | INFO | admin | Items fetched: 5 items +2026-05-29 19:53:59 | INFO | admin | Items fetched: 5 items +2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items +2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items +2026-05-29 20:00:58 | INFO | admin | Items fetched: 0 items +2026-05-29 20:01:00 | INFO | admin | Items fetched: 0 items +2026-05-29 20:01:03 | INFO | admin | Items fetched: 0 items +2026-05-29 20:02:53 | INFO | admin | Items fetched: 0 items +2026-05-29 20:03:50 | INFO | admin | Items fetched: 0 items +2026-05-29 20:03:51 | INFO | admin | Items fetched: 0 items +2026-05-29 20:03:53 | INFO | admin | Items fetched: 0 items +2026-05-29 20:05:26 | INFO | admin | Items fetched: 0 items +2026-05-29 20:11:18 | INFO | admin | Items fetched: 0 items +2026-05-29 20:11:59 | INFO | admin | Items fetched: 0 items +2026-05-29 20:12:12 | INFO | admin | Items fetched: 0 items +2026-05-29 20:15:16 | INFO | admin | Items fetched: 0 items +2026-05-29 20:15:27 | INFO | admin | Items fetched: 0 items +2026-05-29 20:20:21 | INFO | admin | Items fetched: 0 items +2026-05-29 20:29:30 | INFO | admin | Items fetched: 0 items +2026-05-29 20:29:33 | INFO | admin | Items fetched: 0 items +2026-05-29 20:29:40 | INFO | admin | Item created: سفر +2026-05-29 20:29:40 | INFO | admin | Items fetched: 1 items +2026-05-29 20:48:28 | INFO | admin | Items fetched: 1 items +2026-05-29 20:48:31 | INFO | admin | Items fetched: 1 items +2026-05-29 20:48:45 | INFO | admin | Item created: طلا +2026-05-29 20:48:45 | INFO | admin | Items fetched: 2 items +2026-05-29 20:48:52 | INFO | user | User fetched items: 2 items +2026-05-29 20:48:55 | INFO | user | Spin result: سفر +2026-05-29 20:49:35 | INFO | admin | Item deleted: 1 +2026-05-29 20:49:35 | INFO | admin | Items fetched: 1 items +2026-05-29 20:50:46 | INFO | admin | Items fetched: 1 items +2026-05-29 20:51:12 | INFO | admin | Item created: خانه +2026-05-29 20:51:12 | INFO | admin | Items fetched: 2 items +2026-05-29 20:51:18 | INFO | user | User fetched items: 2 items +2026-05-29 20:51:23 | INFO | user | User fetched items: 2 items +2026-05-29 20:51:26 | INFO | user | User fetched items: 2 items +2026-05-29 20:51:40 | INFO | admin | Item created: زمين +2026-05-29 20:51:40 | INFO | admin | Items fetched: 3 items +2026-05-29 20:51:50 | INFO | user | User fetched items: 3 items +2026-05-29 20:51:52 | INFO | user | Spin result: طلا +2026-05-29 20:52:07 | INFO | user | Spin result: زمين +2026-05-29 20:52:13 | INFO | user | Spin result: طلا +2026-05-29 21:00:39 | INFO | user | User fetched items: 3 items +2026-05-29 21:00:41 | INFO | user | Spin result: زمين +2026-05-29 21:02:05 | INFO | user | User fetched items: 3 items +2026-05-29 21:02:09 | INFO | user | Spin result: طلا +2026-05-29 21:02:20 | INFO | user | Spin result: خانه +2026-05-29 21:04:48 | INFO | user | User fetched items: 3 items +2026-05-29 21:04:56 | INFO | user | Spin result: خانه +2026-05-29 21:05:32 | INFO | user | User fetched items: 3 items +2026-05-29 21:06:04 | INFO | user | Spin result: زمين +2026-05-29 21:20:30 | INFO | admin | Item deleted: 2 +2026-05-29 21:20:30 | INFO | admin | Items fetched: 2 items +2026-05-29 21:20:33 | INFO | admin | Item deleted: 3 +2026-05-29 21:20:33 | INFO | admin | Items fetched: 1 items +2026-05-29 21:20:34 | INFO | admin | Item deleted: 4 +2026-05-29 21:20:34 | INFO | admin | Items fetched: 0 items +2026-05-29 21:21:07 | INFO | admin | Item created: پوچ +2026-05-29 21:21:07 | INFO | admin | Items fetched: 1 items +2026-05-29 21:21:18 | INFO | admin | Item created: تخفيف يک ماهه +2026-05-29 21:21:18 | INFO | admin | Items fetched: 2 items +2026-05-29 21:21:47 | INFO | admin | Item created: تخفيف يک ساله +2026-05-29 21:21:47 | INFO | admin | Items fetched: 3 items +2026-05-29 21:22:08 | INFO | admin | Item created: تخفيف سه ماهه +2026-05-29 21:22:08 | INFO | admin | Items fetched: 4 items +2026-05-29 21:22:36 | INFO | admin | Item created: 10% تخفيف يک ماهه +2026-05-29 21:22:36 | INFO | admin | Items fetched: 5 items +2026-05-29 21:22:44 | INFO | admin | Item created: پوچ +2026-05-29 21:22:44 | INFO | admin | Items fetched: 6 items +2026-05-29 21:22:50 | INFO | user | User fetched items: 6 items +2026-05-29 21:22:53 | INFO | user | Spin result: تخفيف سه ماهه +2026-05-29 21:28:31 | INFO | user | Spin result: تخفيف يک ماهه +2026-05-29 21:30:11 | INFO | user | Spin result: 10% تخفيف يک ماهه +2026-05-29 23:55:52 | INFO | admin | Items fetched: 6 items +2026-05-29 23:56:28 | INFO | admin | Items fetched: 6 items +2026-05-29 23:56:41 | INFO | admin | Items fetched: 6 items +2026-05-30 00:02:00 | INFO | admin | Items fetched: 6 items +2026-05-30 00:02:05 | INFO | admin | Item updated: 1 +2026-05-30 00:02:05 | INFO | admin | Items fetched: 6 items +2026-05-30 00:02:13 | INFO | admin | Item updated: 1 +2026-05-30 00:02:13 | INFO | admin | Items fetched: 6 items +2026-05-30 00:02:19 | INFO | admin | Item updated: 1 +2026-05-30 00:02:19 | INFO | admin | Items fetched: 6 items +2026-05-30 00:02:36 | INFO | user | Spin result: پوچ +2026-05-30 00:05:46 | INFO | user | Spin result: تخفيف يک ماهه +2026-05-30 00:13:50 | INFO | user | User fetched items: 6 items +2026-05-30 00:14:24 | INFO | user | User fetched items: 6 items +2026-05-30 00:31:05 | INFO | user | User fetched items: 6 items +2026-05-30 00:33:44 | INFO | user | Spin result: تخفيف يک ساله +2026-05-30 00:43:06 | INFO | user | User fetched items: 6 items +2026-05-30 00:43:10 | INFO | user | Spin result: تخفيف يک ماهه +2026-05-30 00:45:02 | INFO | user | User fetched items: 6 items +2026-05-30 00:45:04 | INFO | user | Spin result: 10% تخفيف يک ماهه +2026-05-30 00:45:41 | INFO | user | Spin result: تخفيف يک ساله +2026-05-30 00:46:22 | INFO | admin | Item created: تخفیف هفتگی +2026-05-30 00:46:22 | INFO | admin | Items fetched: 7 items +2026-05-30 00:46:30 | INFO | user | User fetched items: 7 items +2026-05-30 00:47:47 | INFO | user | Spin result: تخفيف يک ماهه +2026-05-30 00:51:23 | INFO | user | User fetched items: 7 items +2026-05-30 00:51:28 | INFO | user | Spin result: تخفيف سه ماهه +2026-05-30 00:58:22 | INFO | user | User fetched items: 7 items +2026-05-30 01:02:03 | INFO | user | User fetched items: 7 items +2026-05-30 01:04:14 | INFO | admin | Item created: پوچ +2026-05-30 01:04:14 | INFO | admin | Items fetched: 8 items +2026-05-30 01:05:21 | INFO | user | User fetched items: 8 items +2026-05-30 01:12:18 | INFO | user | Spin result: پوچ +2026-05-30 01:12:25 | INFO | user | Spin result: تخفيف سه ماهه diff --git a/backend/logs/error.log b/backend/logs/error.log index e69de29..9d08148 100644 --- a/backend/logs/error.log +++ b/backend/logs/error.log @@ -0,0 +1,40 @@ +2026-05-24 12:00:16 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:43 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:47 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:57 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:00:59 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:01:05 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:42 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items +[SQL: SELECT items.id AS items_id, items.title AS items_title +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:49 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:09:51 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-05-24 12:12:18 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items +[SQL: INSERT INTO items (title) VALUES (?)] +[parameters: ('جایزه 1',)] +(Background on this error at: https://sqlalche.me/e/20/e3q8) diff --git a/backend/main.py b/backend/main.py index 003c12e..fa62976 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,21 +1,29 @@ 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.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, - allow_origins=["*"], + allow_origins=[ + "http://localhost:3000", + "http://127.0.0.1:3000", + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) - -app.include_router(admin_router, prefix="/admin") -app.include_router(user_router, prefix="/user") \ No newline at end of file +app.include_router(admin_router, prefix="/admin") \ No newline at end of file diff --git a/backend/schemas.py b/backend/schemas.py index e36081c..45c04a2 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,11 +1,20 @@ from pydantic import BaseModel + class ItemCreate(BaseModel): title: str + value: int + + +class ItemUpdate(BaseModel): + title: str + value: int + class ItemResponse(BaseModel): id: int title: str + value: int 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 a988918..3c9f161 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 b9545a4..c17828a 100644 --- a/backend/services/crud.py +++ b/backend/services/crud.py @@ -1,61 +1,35 @@ import random - from sqlalchemy.orm import Session - from app.db.models import Item - -def get_items( - db: Session -): - +def get_items(db: Session): return db.query(Item).all() - -def create_item( - db: Session, - title: str -): - - item = Item( - title=title - ) - +def create_item(db: Session, title: str, value: int): # ✅ prize → value + item = Item(title=title, value=value) # ✅ prize → value db.add(item) - db.commit() - db.refresh(item) - return item - -def delete_item( - db: Session, - item_id: int -): - - item = db.query(Item).filter( - Item.id == item_id - ).first() - +def update_item(db: Session, item_id: int, title: str, value: int): # ✅ prize → value + item = db.query(Item).filter(Item.id == item_id).first() if item: - - db.delete(item) - + item.title = title + item.value = value # ✅ prize → value db.commit() - + db.refresh(item) return item +def delete_item(db: Session, item_id: int): + item = db.query(Item).filter(Item.id == item_id).first() + if item: + db.delete(item) + db.commit() + return item -def spin_wheel( - db: Session -): - +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 diff --git a/backend/wheel.db b/backend/spinwheel.db similarity index 56% rename from backend/wheel.db rename to backend/spinwheel.db index 61ec360..f2d2d47 100644 Binary files a/backend/wheel.db and b/backend/spinwheel.db differ diff --git a/frontend/admin.html b/frontend/admin.html index 0ae1a68..462e254 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -1,20 +1,92 @@ + - پنل ادمین + + + + پنل مدیریت + - -

مدیریت آیتم‌ها

-
- - + + +
+ +
+

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

+

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

+
+ + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + +
#عنوان جایزهمقدارعملیات
+ +
+
- - + + \ No newline at end of file diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index b183966..5f262d2 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -1,61 +1,325 @@ +/* ========================= + ADMIN PANEL +========================= */ + +.admin-container { + width: 100%; + max-width: 1200px; + padding: 30px; +} + +.admin-header { + text-align: center; + margin-bottom: 30px; +} + +.admin-header h1 { + color: #FFD700; + font-size: 38px; + text-shadow: 0 0 15px rgba(255,215,0,0.5); +} + +.admin-header p { + color: #f5d6a0; + margin-top: 10px; + font-size: 18px; +} + +.add-section { + background: rgba(0,0,0,0.25); + backdrop-filter: blur(8px); + + border: 1px solid rgba(255,255,255,0.1); + + border-radius: 20px; + + padding: 20px; + + display: flex; + gap: 15px; + + margin-bottom: 30px; + + box-shadow: + 0 10px 30px rgba(0,0,0,0.4), + inset 0 0 20px rgba(255,255,255,0.03); +} + +.add-section input { + flex: 1; + + padding: 14px; + + border: none; + + border-radius: 12px; + + background: rgba(255,255,255,0.08); + + color: white; + + font-size: 16px; + + outline: none; +} + +.add-section input::placeholder { + color: rgba(255,255,255,0.5); +} + +#add-btn { + padding: 14px 28px; + + background: linear-gradient( + 135deg, + #FFD700, + #D4AC0D + ); + + color: #4a0f0f; + + border: none; + + border-radius: 14px; + + font-weight: bold; + + cursor: pointer; + + transition: 0.2s; + + box-shadow: + 0 4px 20px rgba(255,215,0,0.3); +} + +#add-btn:hover { + transform: translateY(-2px); + + box-shadow: + 0 8px 25px rgba(255,215,0,0.45); +} + +.table-wrapper { + background: rgba(0,0,0,0.25); + + backdrop-filter: blur(10px); + + border-radius: 22px; + + overflow: hidden; + + border: 1px solid rgba(255,255,255,0.08); + + box-shadow: + 0 10px 40px rgba(0,0,0,0.45); +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: + linear-gradient( + 135deg, + #FFD700, + #D4AC0D + ); + + color: #4a0f0f; +} + +th { + padding: 18px; + font-size: 17px; +} + +td { + padding: 18px; + text-align: center; + color: white; +} + +tbody tr { + border-bottom: + 1px solid rgba(255,255,255,0.06); + + transition: 0.2s; +} + +tbody tr:hover { + background: + rgba(255,255,255,0.05); +} + +.action-buttons { + display: flex; + justify-content: center; + gap: 10px; +} + +.edit-btn, +.delete-btn { + border: none; + + padding: 10px 16px; + + border-radius: 10px; + + cursor: pointer; + + font-weight: bold; + + transition: 0.2s; +} + +.edit-btn { + background: + linear-gradient( + 135deg, + #9B59B6, + #7D3C98 + ); + + color: white; +} + +.delete-btn { + background: + linear-gradient( + 135deg, + #E74C3C, + #922B21 + ); + + color: white; +} + +.edit-btn:hover, +.delete-btn:hover { + transform: scale(1.05); +} + +@media (max-width: 768px) { + + .add-section { + flex-direction: column; + } + + table { + font-size: 14px; + } + + th, + td { + padding: 12px; + } +} + + +/* ========================= + WHEEL PAGE +========================= */ + * { - box-sizing: border-box; margin: 0; padding: 0; + box-sizing: border-box; } body { + min-height: 100vh; + background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%); + display: flex; + justify-content: center; + align-items: center; font-family: Tahoma, sans-serif; - background: #f5f5f5; +} + +.page { display: flex; flex-direction: column; align-items: center; - padding: 40px 20px; - gap: 20px; + gap: 24px; + padding: 30px; } -h1 { - color: #2c3e50; - margin-bottom: 10px; +.page h1 { + color: #FFD700; + font-size: 36px; + text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); + letter-spacing: 2px; } -canvas { - border-radius: 50%; - box-shadow: 0 4px 20px rgba(0,0,0,0.2); +.wheel-wrapper { + position: relative; + width: 500px; + height: 500px; } -button { - padding: 12px 30px; - font-size: 18px; +.wheel-wrapper canvas { + display: block; + width: 100%; + height: 100%; +} + +#spin-btn { + padding: 16px 48px; + font-size: 20px; + font-weight: bold; font-family: Tahoma, sans-serif; - background: #e74c3c; - color: white; + background: linear-gradient(135deg, #FFD700, #D4AC0D); + color: #4a0f0f; border: none; - border-radius: 8px; + border-radius: 50px; cursor: pointer; + box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4); + transition: 0.2s; } -button:hover { - background: #c0392b; +#spin-btn:hover { + transform: translateY(-3px); + box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55); } -button:disabled { - background: #aaa; +#spin-btn:disabled { + opacity: 0.5; cursor: not-allowed; + transform: none; } #result { - font-size: 24px; + min-height: 50px; + text-align: center; +} + +.winner-text { + font-size: 28px; font-weight: bold; - color: #2c3e50; + color: #FFD700; + text-shadow: 0 0 20px rgba(255, 215, 0, 0.7); + animation: pop 0.4s ease; } -/* ادمین */ -#add-section { - display: flex; - gap: 10px; - margin-bottom: 20px; +@keyframes pop { + 0% { transform: scale(0.5); opacity: 0; } + 70% { transform: scale(1.1); } + 100% { transform: scale(1); opacity: 1; } } -input { - padding: 10px; \ No newline at end of file +@media (max-width: 550px) { + .wheel-wrapper { + width: 340px; + height: 340px; + } + + .page h1 { + font-size: 26px; + } + + #spin-btn { + font-size: 16px; + padding: 12px 36px; + } +} \ No newline at end of file diff --git a/frontend/assets/js/admin.js b/frontend/assets/js/admin.js index 2058645..c81efca 100644 --- a/frontend/assets/js/admin.js +++ b/frontend/assets/js/admin.js @@ -1,33 +1,103 @@ -import { adminGetItems, createItem, deleteItem } from './api.js'; +import { + adminGetItems, + createItem, + deleteItem, + updateItem // 👈 باید اینو هم به api.js اضافه کنی +} from './api.js'; -const list = document.getElementById('items-list'); -const input = document.getElementById('new-item'); +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'); +let editId = null; // 👈 حالت ویرایش + +// -------------------- Render -------------------- async function renderItems() { const items = await adminGetItems(); - list.innerHTML = ''; - items.forEach(item => { - const li = document.createElement('li'); - li.innerHTML = ` - ${item.title} - + + tableBody.innerHTML = ''; + + items.forEach((item, index) => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${index + 1} + ${item.title} + ${item.value} + + + + + + + `; - list.appendChild(li); + + tableBody.appendChild(row); }); } +// -------------------- Delete -------------------- window.removeItem = async (id) => { await deleteItem(id); renderItems(); }; +// -------------------- Edit -------------------- +window.editItem = (id, title, value) => { + titleInput.value = title; + valueInput.value = value; + + editId = id; + + addBtn.textContent = 'آپدیت ✏️'; +}; + +// -------------------- Add / Update -------------------- addBtn.onclick = async () => { - const title = input.value.trim(); - if (!title) return; - await createItem(title); - input.value = ''; + 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 = ''; + renderItems(); }; +// -------------------- Init -------------------- renderItems(); \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js index a55795f..17958bc 100644 --- a/frontend/assets/js/api.js +++ b/frontend/assets/js/api.js @@ -1,20 +1,40 @@ const BASE = 'http://127.0.0.1:8000'; 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(r => r.json()); export const adminGetItems = () => - fetch(`${BASE}/admin/items`).then(r => r.json()); + fetch(`${BASE}/admin/items`) + .then(r => r.json()); -export const createItem = (title) => +export const createItem = (data) => fetch(`${BASE}/admin/items`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title }) + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(r => r.json()); + +export const updateItem = (id, data) => + fetch(`${BASE}/admin/items/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) }).then(r => r.json()); export const deleteItem = (id) => - fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(r => r.json()); \ No newline at end of file + fetch(`${BASE}/admin/items/${id}`, { + method: 'DELETE' + }).then(r => { + if (r.status === 204) return {}; + return r.json(); + }); \ No newline at end of file diff --git a/frontend/assets/js/wheel.js b/frontend/assets/js/wheel.js index 83cc99b..e4ca45c 100644 --- a/frontend/assets/js/wheel.js +++ b/frontend/assets/js/wheel.js @@ -4,43 +4,309 @@ const canvas = document.getElementById('wheel'); const ctx = canvas.getContext('2d'); const btn = document.getElementById('spin-btn'); const result = document.getElementById('result'); -const colors = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c']; + +const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' }; +const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' }; let items = []; let angle = 0; let spinning = false; +let lampOn = true; async function init() { items = await getItems(); - drawWheel(angle); + draw(angle); + setInterval(() => { + lampOn = !lampOn; + draw(angle); + }, 600); } -function drawWheel(rotation) { +function getTheme(i, total) { + if (total % 2 === 0) { + return i % 2 === 0 ? DARK : LIGHT; + } else { + if (i === total - 1) return DARK; + return i % 2 === 0 ? DARK : LIGHT; + } +} + +function drawSegment(cx, cy, r, start, end, theme) { + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r, start, end); + ctx.closePath(); + ctx.fillStyle = theme.edge; + ctx.fill(); + + const mid = (start + end) / 2; + const gx = cx + r * 0.5 * Math.cos(mid); + const gy = cy + r * 0.5 * Math.sin(mid); + const grad = ctx.createRadialGradient(gx, gy, 0, cx, cy, r); + grad.addColorStop(0, theme.highlight); + grad.addColorStop(0.45, theme.main); + grad.addColorStop(1, theme.edge); + + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r - 2, start, end); + ctx.closePath(); + ctx.fillStyle = grad; + ctx.fill(); + + const hGrad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.35); + hGrad.addColorStop(0, 'rgba(255,255,255,0.28)'); + hGrad.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r - 2, start, end); + ctx.closePath(); + ctx.fillStyle = hGrad; + ctx.fill(); +} + +function draw(rotation) { if (items.length === 0) return; const cx = canvas.width / 2; const cy = canvas.height / 2; - const r = cx - 10; + const r = cx - 40; const slice = (2 * Math.PI) / items.length; + const total = items.length; ctx.clearRect(0, 0, canvas.width, canvas.height); + + // ═══ سایه کلی ═══ + ctx.save(); + ctx.shadowColor = 'rgba(0,0,0,0.6)'; + ctx.shadowBlur = 30; + ctx.shadowOffsetX = 5; + ctx.shadowOffsetY = 8; + ctx.beginPath(); + ctx.arc(cx, cy, r + 40, 0, Math.PI * 2); + ctx.fillStyle = '#1A0008'; + ctx.fill(); + ctx.restore(); + + // ═══ حلقه بیرونی تیره ═══ + const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40); + rimGrad.addColorStop(0, '#A0003A'); + rimGrad.addColorStop(0.3, '#6D0025'); + rimGrad.addColorStop(0.7, '#3D0015'); + rimGrad.addColorStop(1, '#1A0008'); + ctx.beginPath(); + ctx.arc(cx, cy, r + 40, 0, Math.PI * 2); + ctx.fillStyle = rimGrad; + ctx.fill(); + + // ═══ بخش‌های گردونه ═══ items.forEach((item, i) => { 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( + cx, cy, + cx + r * Math.cos(lineAngle), + cy + r * Math.sin(lineAngle) + ); + lGrad.addColorStop(0, 'rgba(700,550,0,5)'); + lGrad.addColorStop(0.4, '#a2922e'); + lGrad.addColorStop(1, '#c8b43b'); ctx.beginPath(); ctx.moveTo(cx, cy); - ctx.arc(cx, cy, r, start, end); - ctx.fillStyle = colors[i % colors.length]; - ctx.fill(); + ctx.lineTo(cx + r * Math.cos(lineAngle), cy + r * Math.sin(lineAngle)); + ctx.strokeStyle = lGrad; + ctx.lineWidth = 5; 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.fillStyle = '#fff'; - ctx.font = 'bold 16px Tahoma'; - ctx.fillText(item.title, r - 10, 5); + ctx.fillStyle = theme.text; + ctx.font = 'bold 12px Tahoma'; + ctx.shadowColor = 'rgba(0,0,0,0.6)'; + ctx.shadowBlur = 4; + const text = item.title; + const maxWidth = r - 52; + if (ctx.measureText(text).width > maxWidth) { + const mid = Math.ceil(text.length / 2); + ctx.fillText(text.slice(0, mid), r - 20, -7); + ctx.fillText(text.slice(mid), r - 20, 9); + } else { + ctx.fillText(text, r - 20, 5); + } ctx.restore(); }); + + // ═══ حلقه طلایی داخلی ═══ + ctx.beginPath(); + ctx.arc(cx, cy, r + 2, 0, Math.PI * 2); + ctx.strokeStyle = '#dfd498'; + ctx.lineWidth = 4.5; + ctx.shadowColor = 'rgba(255,215,0,0.9)'; + ctx.shadowBlur = 10; + ctx.stroke(); + ctx.shadowBlur = 0; + + // ═══ لامپ‌ها ═══ + const lampCount = 24; + const lampR = r + 23; + for (let i = 0; i < lampCount; i++) { + const lampAngle = (i / lampCount) * Math.PI * 2; + const lx = cx + lampR * Math.cos(lampAngle); + const ly = cy + lampR * Math.sin(lampAngle); + const isOn = lampOn ? i % 2 === 0 : i % 2 !== 0; + + if (isOn) { + const glowGrad = ctx.createRadialGradient(lx, ly, 0, lx, ly, 14); + glowGrad.addColorStop(0, 'rgba(255,220,80,0.55)'); + glowGrad.addColorStop(1, 'rgba(255,220,80,0)'); + ctx.beginPath(); + ctx.arc(lx, ly, 14, 0, Math.PI * 2); + ctx.fillStyle = glowGrad; + ctx.fill(); + } + + const lGrad = ctx.createRadialGradient(lx - 2.5, ly - 2.5, 0.5, lx, ly, 7.5); + if (isOn) { + lGrad.addColorStop(0, '#FFFFF5'); + lGrad.addColorStop(0.2, '#FFE566'); + lGrad.addColorStop(0.6, '#FFB300'); + lGrad.addColorStop(1, '#E65100'); + ctx.shadowColor = 'rgba(255,180,0,1)'; + ctx.shadowBlur = 14; + } else { + lGrad.addColorStop(0, '#B8954A'); + lGrad.addColorStop(1, '#4A3010'); + } + + ctx.beginPath(); + ctx.arc(lx, ly, 7.5, 0, Math.PI * 2); + ctx.fillStyle = lGrad; + ctx.fill(); + ctx.shadowBlur = 0; + + ctx.beginPath(); + ctx.arc(lx, ly, 7.5, 0, Math.PI * 2); + ctx.strokeStyle = isOn ? '#C8960C' : '#6A4A18'; + ctx.lineWidth = 1.2; + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(lx - 2.5, ly - 2.5, 2.5, 0, Math.PI * 2); + ctx.fillStyle = isOn ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.2)'; + ctx.fill(); + } + + // ═══ حلقه طلایی بیرونی ═══ + ctx.beginPath(); + ctx.arc(cx, cy, r + 35, 0, Math.PI * 2); + ctx.strokeStyle = '#C8960C'; + ctx.lineWidth = 4.5; + ctx.shadowColor = 'rgba(150,100,12,0.5)'; + ctx.shadowBlur = 6; + ctx.stroke(); + ctx.shadowBlur = 0; + + // ═══ دایره مرکزی — هماهنگ با نشانگر ═══ + const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22); + cGrad.addColorStop(0, '#FFFFF0'); + cGrad.addColorStop(0.15, '#FFE566'); + cGrad.addColorStop(0.45, '#FFD700'); + cGrad.addColorStop(0.75, '#ff962a'); + cGrad.addColorStop(1, '#b37900'); + + ctx.beginPath(); + ctx.arc(cx, cy, 22, 0, Math.PI * 2); + ctx.fillStyle = cGrad; + ctx.shadowColor = 'rgba(0,0,0,0.6)'; + ctx.shadowBlur = 14; + ctx.fill(); + ctx.shadowBlur = 0; + + ctx.beginPath(); + ctx.arc(cx, cy, 22, 0, Math.PI * 2); + ctx.strokeStyle = '#8B6914'; + ctx.lineWidth = 2; + ctx.stroke(); + + // بازتاب روی دایره مرکزی + ctx.beginPath(); + ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fill(); + + // ═══ نشانگر ═══ + drawPointer(cx); +} + +function drawPointer(cx) { + // سایه + ctx.save(); + ctx.shadowColor = 'rgba(0,0,0,0.4)'; + ctx.shadowBlur = 8; + ctx.shadowOffsetX = 3; + ctx.shadowOffsetY = 4; + ctx.beginPath(); + ctx.arc(cx, 20, 17, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0,0,0,0.1)'; + ctx.fill(); + ctx.restore(); + + // دایره طلایی — همرنگ مرکز + const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18); + pGrad.addColorStop(0, '#FFFFF0'); + pGrad.addColorStop(0.15, '#FFE566'); + pGrad.addColorStop(0.45, '#FFD700'); + pGrad.addColorStop(0.75, '#ff962a'); + pGrad.addColorStop(1, '#b37900'); + + ctx.beginPath(); + ctx.arc(cx, 20, 17, 0, Math.PI * 2); + ctx.fillStyle = pGrad; + ctx.shadowColor = 'rgba(200,150,0,0.8)'; + ctx.shadowBlur = 12; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = '#8B6914'; + ctx.lineWidth = 2; + ctx.stroke(); + + // بازتاب + ctx.beginPath(); + ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255,255,255,0.35)'; + ctx.fill(); + + // مثلث — همرنگ + const triGrad = ctx.createLinearGradient(cx, 34, cx, 62); + triGrad.addColorStop(0, '#FFE566'); + triGrad.addColorStop(0.35, '#FFD700'); + triGrad.addColorStop(1, '#b37900'); + + ctx.beginPath(); + ctx.moveTo(cx - 13, 35); + ctx.lineTo(cx + 13, 35); + ctx.lineTo(cx, 62); + ctx.closePath(); + ctx.fillStyle = triGrad; + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = 8; + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = '#8B6914'; + ctx.lineWidth = 1.5; + ctx.stroke(); } btn.onclick = async () => { @@ -53,26 +319,30 @@ btn.onclick = async () => { const winner = data.winner; const winnerIdx = items.findIndex(i => i.id === winner.id); const slice = (2 * Math.PI) / items.length; - const targetAngle = angle + (Math.PI * 2 * 5) + - (Math.PI * 2 - (winnerIdx * slice + slice / 2)); - const duration = 4000; - const start = performance.now(); + const targetAngle = + angle + + Math.PI * 2 * 8 + + (-Math.PI / 2 - (winnerIdx * slice + slice / 2) - angle % (Math.PI * 2) + Math.PI * 2 * 3); + + const duration = 5500; + const startTime = performance.now(); const from = angle; function animate(now) { - const elapsed = now - start; + const elapsed = now - startTime; const t = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - t, 3); + const ease = 1 - Math.pow(1 - t, 4); angle = from + (targetAngle - from) * ease; - drawWheel(angle); + draw(angle); if (t < 1) { requestAnimationFrame(animate); } else { spinning = false; btn.disabled = false; - result.textContent = `🎉 ${winner.title}`; + draw(angle); + result.innerHTML = `🎉 ${winner.title} 🎉`; } } diff --git a/frontend/index.html b/frontend/index.html index 0713603..a2039d2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,12 +6,14 @@ -

گردونه شانس

- -
- -

- +
+

گردونه شانس

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