Compare commits
4 Commits
ed82bf7f1f
...
spin-wheel
| Author | SHA1 | Date | |
|---|---|---|---|
| 3db63569e6 | |||
| df3ccb1886 | |||
| ec86cdc528 | |||
| b0ff339525 |
@@ -1,4 +1,3 @@
|
||||
DATABASE_URL=sqlite:///./wheel.db
|
||||
|
||||
DATABASE_URL=sqlite:///./spinwheel.db
|
||||
LOG_ENABLED=true
|
||||
LOG_LEVEL=DEBUG
|
||||
329
backend/README.md
Normal file
329
backend/README.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Spin Wheel System
|
||||
|
||||
A web-based Lucky Wheel application developed with **FastAPI**, **JavaScript**, and **SQLite**. The system allows administrators to manage rewards, configure winning probabilities, customize the wheel appearance, and provide an engaging user experience with animations and sound effects.
|
||||
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
## User Features
|
||||
|
||||
* Interactive spin wheel
|
||||
* Smooth spinning animations
|
||||
* Sound effects during wheel rotation
|
||||
* Winner notification modal
|
||||
* Reward description display
|
||||
* Responsive user interface
|
||||
* Customizable page title
|
||||
* Customizable background color
|
||||
|
||||
## Admin Features
|
||||
|
||||
* Create, edit, and delete rewards
|
||||
* Configure reward probabilities
|
||||
* Manage reward descriptions
|
||||
* Configure wheel page title
|
||||
* Customize wheel appearance
|
||||
* Manage reward types
|
||||
* Real-time reward management
|
||||
|
||||
---
|
||||
|
||||
# Supported Reward Types
|
||||
|
||||
The system supports three reward categories:
|
||||
|
||||
| Reward Type | Description |
|
||||
| ------------- | ------------------------- |
|
||||
| Empty | No reward |
|
||||
| Discount Code | Promotional discount code |
|
||||
| LiveCoin (LC) | Virtual currency reward |
|
||||
|
||||
---
|
||||
|
||||
# Technology Stack
|
||||
|
||||
## Backend
|
||||
|
||||
* Python 3.10+
|
||||
* FastAPI
|
||||
* SQLAlchemy
|
||||
* Alembic
|
||||
* SQLite
|
||||
|
||||
## Frontend
|
||||
|
||||
* HTML5
|
||||
* CSS3
|
||||
* JavaScript (Vanilla JS)
|
||||
* Dana Font
|
||||
|
||||
## Development Tools
|
||||
|
||||
* Git
|
||||
* Uvicorn
|
||||
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
```text
|
||||
spin-wheel/
|
||||
│
|
||||
├── backend/
|
||||
│ ├── alembic/
|
||||
│ ├── api/
|
||||
│ │ ├── admin.py
|
||||
│ │ └── user.py
|
||||
│ │
|
||||
│ ├── app/
|
||||
│ ├── core/
|
||||
│ ├── logs/
|
||||
│ ├── services/
|
||||
│ │
|
||||
│ ├── main.py
|
||||
│ ├── schemas.py
|
||||
│ ├── config.py
|
||||
│ ├── requirements.txt
|
||||
│ └── spinwheel.db
|
||||
│
|
||||
├── frontend/
|
||||
│ ├── assets/
|
||||
│ │ ├── css/
|
||||
│ │ └── fonts/
|
||||
│ │
|
||||
│ ├── js/
|
||||
│ │ ├── admin.js
|
||||
│ │ └── api.js
|
||||
│ │
|
||||
│ ├── admin.html
|
||||
│ └── index.html
|
||||
│
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
## 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd spin-wheel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Create a Virtual Environment
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
### Linux / macOS
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Install Backend Dependencies
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configure Environment Variables
|
||||
|
||||
Create a `.env` file inside the backend directory if required.
|
||||
|
||||
Example:
|
||||
|
||||
```env
|
||||
DATABASE_URL=sqlite:///spinwheel.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Run Database Migrations
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Start Backend Server
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
Backend API:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
Swagger Documentation:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8000/docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Start Frontend
|
||||
|
||||
Serve the frontend application on port 3000.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
python -m http.server 3000
|
||||
```
|
||||
|
||||
Frontend URL:
|
||||
|
||||
```text
|
||||
http://localhost:3000/index.html
|
||||
http://localhost:3000/admin.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# API Endpoints
|
||||
|
||||
## User APIs
|
||||
|
||||
### Get Rewards
|
||||
|
||||
```http
|
||||
GET /user/items
|
||||
```
|
||||
|
||||
### Spin the Wheel
|
||||
|
||||
```http
|
||||
POST /user/spin
|
||||
```
|
||||
|
||||
Example Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"reward_type": "DISCOUNT_CODE",
|
||||
"title": "20% Discount",
|
||||
"description": "Get 20% off on your next purchase.",
|
||||
"probability": 15
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin APIs
|
||||
|
||||
### Get Rewards
|
||||
|
||||
```http
|
||||
GET /admin/items
|
||||
```
|
||||
|
||||
### Create Reward
|
||||
|
||||
```http
|
||||
POST /admin/items
|
||||
```
|
||||
|
||||
### Update Reward
|
||||
|
||||
```http
|
||||
PUT /admin/items/{id}
|
||||
```
|
||||
|
||||
### Delete Reward
|
||||
|
||||
```http
|
||||
DELETE /admin/items/{id}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Reward Probability System
|
||||
|
||||
Each reward has a configurable probability value.
|
||||
|
||||
The winner is selected using a weighted random selection algorithm.
|
||||
|
||||
Example:
|
||||
|
||||
| Reward | Probability |
|
||||
| ------------- | ----------- |
|
||||
| Empty | 50 |
|
||||
| Discount Code | 30 |
|
||||
| LiveCoin | 20 |
|
||||
|
||||
Higher values increase the chance of winning that reward.
|
||||
|
||||
---
|
||||
|
||||
# Customization Options
|
||||
|
||||
The admin panel allows configuration of:
|
||||
|
||||
* Wheel page title
|
||||
* Background color
|
||||
* Reward descriptions
|
||||
* Reward probabilities
|
||||
* Reward types
|
||||
* Wheel appearance
|
||||
|
||||
---
|
||||
|
||||
# User Experience Enhancements
|
||||
|
||||
* Wheel spinning animation
|
||||
* Sound effects
|
||||
* Winner modal popup
|
||||
* Reward details display
|
||||
* Modern Persian typography using Dana Font
|
||||
|
||||
---
|
||||
|
||||
# Future Improvements
|
||||
|
||||
* Authentication & Authorization
|
||||
* User Accounts
|
||||
* Spin History
|
||||
* Analytics Dashboard
|
||||
* Campaign Management
|
||||
* Multi-language Support
|
||||
* Docker Deployment
|
||||
|
||||
---
|
||||
|
||||
# Security Recommendations
|
||||
|
||||
For production environments:
|
||||
|
||||
* Enable HTTPS
|
||||
* Protect admin endpoints
|
||||
* Validate all incoming requests
|
||||
* Store secrets in environment variables
|
||||
* Configure proper CORS policies
|
||||
|
||||
---
|
||||
|
||||
# License
|
||||
|
||||
This project is intended for educational purposes and commercial lucky wheel campaigns. It can be extended and customized based on business requirements.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
backend/alembic/versions/b55808a86947_add_prize_column.py
Normal file
28
backend/alembic/versions/b55808a86947_add_prize_column.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""add prize column
|
||||
|
||||
Revision ID: b55808a86947
|
||||
Revises: ab09ab5070f7
|
||||
Create Date: 2026-05-29 19:32:41.615653
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b55808a86947'
|
||||
down_revision: Union[str, Sequence[str], None] = 'ab09ab5070f7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
28
backend/alembic/versions/eb35214a1411_update_items_table.py
Normal file
28
backend/alembic/versions/eb35214a1411_update_items_table.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""update items table
|
||||
|
||||
Revision ID: eb35214a1411
|
||||
Revises: f71dc2bbdfc6
|
||||
Create Date: 2026-06-03 02:15:20.584639
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'eb35214a1411'
|
||||
down_revision: Union[str, Sequence[str], None] = 'f71dc2bbdfc6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
28
backend/alembic/versions/f71dc2bbdfc6_update_items_table.py
Normal file
28
backend/alembic/versions/f71dc2bbdfc6_update_items_table.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""update items table
|
||||
|
||||
Revision ID: f71dc2bbdfc6
|
||||
Revises: b55808a86947
|
||||
Create Date: 2026-06-03 02:12:35.370477
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f71dc2bbdfc6'
|
||||
down_revision: Union[str, Sequence[str], None] = 'b55808a86947'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import schemas
|
||||
from schemas import ItemResponse
|
||||
from services import crud
|
||||
@@ -13,7 +9,6 @@ from core.logger import get_logger
|
||||
router = APIRouter()
|
||||
logger = get_logger('admin')
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -21,7 +16,6 @@ def get_db():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# GET ALL ITEMS
|
||||
@router.get("/items", response_model=list[ItemResponse])
|
||||
def get_items(db: Session = Depends(get_db)):
|
||||
@@ -30,31 +24,54 @@ 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
|
||||
|
||||
|
||||
# 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)
|
||||
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.error(f"Error creating item: {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:
|
||||
@@ -65,5 +82,5 @@ def delete_item(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting item: {e}")
|
||||
logger.critical(f"Unexpected error deleting item: {e}")
|
||||
raise
|
||||
@@ -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,29 @@ def get_items(db: Session = Depends(get_db)):
|
||||
def spin(db: Session = Depends(get_db)):
|
||||
try:
|
||||
winner = crud.spin_wheel(db)
|
||||
if not winner:
|
||||
|
||||
if not winner:
|
||||
logger.warning("Spin attempted but no items available")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No items available"
|
||||
)
|
||||
|
||||
logger.info(f"Spin result: {winner.title}")
|
||||
|
||||
return {
|
||||
"winner": {
|
||||
"id": winner.id,
|
||||
"title": winner.title,
|
||||
"prize": winner.prize,
|
||||
"prize_type": winner.prize_type,
|
||||
"description": winner.description or ""
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Unexpected error during spin: {e}")
|
||||
raise
|
||||
Binary file not shown.
@@ -1,21 +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
|
||||
)
|
||||
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)
|
||||
@@ -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()
|
||||
Binary file not shown.
@@ -1 +1,517 @@
|
||||
2026-05-24 09:52:10 | INFO | test | test
|
||||
2026-05-24 12:00:16 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:00:43 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:00:47 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:00:57 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:00:59 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:01:05 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:09:42 | ERROR | admin | Error fetching items: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:09:49 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:09:51 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:12:18 | ERROR | admin | Error creating item: (sqlite3.OperationalError) no such table: items
|
||||
[SQL: INSERT INTO items (title) VALUES (?)]
|
||||
[parameters: ('جایزه 1',)]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-05-24 12:24:46 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-24 12:24:55 | INFO | admin | Item created: جایزه 1
|
||||
2026-05-24 12:24:55 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-24 12:25:07 | INFO | admin | Item created: جایزه 2
|
||||
2026-05-24 12:25:07 | INFO | admin | Items fetched: 2 items
|
||||
2026-05-24 12:25:12 | INFO | admin | Item created: جایزه 3
|
||||
2026-05-24 12:25:12 | INFO | admin | Items fetched: 3 items
|
||||
2026-05-24 12:25:17 | INFO | user | User fetched items: 3 items
|
||||
2026-05-24 12:25:19 | INFO | user | Spin result: جایزه 3
|
||||
2026-05-24 12:26:33 | INFO | admin | Item created: پوچ
|
||||
2026-05-24 12:26:33 | INFO | admin | Items fetched: 4 items
|
||||
2026-05-24 12:26:38 | INFO | user | User fetched items: 4 items
|
||||
2026-05-24 12:26:40 | INFO | user | Spin result: جایزه 1
|
||||
2026-05-24 12:41:01 | INFO | user | User fetched items: 4 items
|
||||
2026-05-24 12:41:02 | INFO | user | User fetched items: 4 items
|
||||
2026-05-24 12:41:04 | INFO | user | Spin result: پوچ
|
||||
2026-05-24 12:45:29 | INFO | user | Spin result: پوچ
|
||||
2026-05-24 12:49:58 | INFO | user | Spin result: جایزه 1
|
||||
2026-05-24 12:50:31 | INFO | user | Spin result: پوچ
|
||||
2026-05-24 12:50:59 | INFO | user | Spin result: جایزه 2
|
||||
2026-05-24 12:51:10 | INFO | user | Spin result: جایزه 3
|
||||
2026-05-24 12:57:40 | INFO | admin | Item deleted: 4
|
||||
2026-05-24 12:57:40 | INFO | admin | Items fetched: 3 items
|
||||
2026-05-24 12:57:48 | INFO | admin | Item created: پوچ
|
||||
2026-05-24 12:57:48 | INFO | admin | Items fetched: 4 items
|
||||
2026-05-24 12:58:01 | INFO | admin | Item created: جایزه 3
|
||||
2026-05-24 12:58:01 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-24 12:58:49 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:43:29 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:43:34 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:43:35 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:44:03 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:44:05 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:44:28 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:45:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:45:18 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:45:19 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:45:22 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:45:23 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:47:15 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:16 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:17 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:18 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:47:20 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:47:21 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:48:16 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:49:38 | INFO | user | User fetched items: 5 items
|
||||
2026-05-29 19:49:45 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:53:58 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:53:59 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 19:54:00 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 20:00:58 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:01:00 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:01:03 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:02:53 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:03:50 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:03:51 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:03:53 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:05:26 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:11:18 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:11:59 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:12:12 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:15:16 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:15:27 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:20:21 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:29:30 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:29:33 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 20:29:40 | INFO | admin | Item created: سفر
|
||||
2026-05-29 20:29:40 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 20:48:28 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 20:48:31 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 20:48:45 | INFO | admin | Item created: طلا
|
||||
2026-05-29 20:48:45 | INFO | admin | Items fetched: 2 items
|
||||
2026-05-29 20:48:52 | INFO | user | User fetched items: 2 items
|
||||
2026-05-29 20:48:55 | INFO | user | Spin result: سفر
|
||||
2026-05-29 20:49:35 | INFO | admin | Item deleted: 1
|
||||
2026-05-29 20:49:35 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 20:50:46 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 20:51:12 | INFO | admin | Item created: خانه
|
||||
2026-05-29 20:51:12 | INFO | admin | Items fetched: 2 items
|
||||
2026-05-29 20:51:18 | INFO | user | User fetched items: 2 items
|
||||
2026-05-29 20:51:23 | INFO | user | User fetched items: 2 items
|
||||
2026-05-29 20:51:26 | INFO | user | User fetched items: 2 items
|
||||
2026-05-29 20:51:40 | INFO | admin | Item created: زمين
|
||||
2026-05-29 20:51:40 | INFO | admin | Items fetched: 3 items
|
||||
2026-05-29 20:51:50 | INFO | user | User fetched items: 3 items
|
||||
2026-05-29 20:51:52 | INFO | user | Spin result: طلا
|
||||
2026-05-29 20:52:07 | INFO | user | Spin result: زمين
|
||||
2026-05-29 20:52:13 | INFO | user | Spin result: طلا
|
||||
2026-05-29 21:00:39 | INFO | user | User fetched items: 3 items
|
||||
2026-05-29 21:00:41 | INFO | user | Spin result: زمين
|
||||
2026-05-29 21:02:05 | INFO | user | User fetched items: 3 items
|
||||
2026-05-29 21:02:09 | INFO | user | Spin result: طلا
|
||||
2026-05-29 21:02:20 | INFO | user | Spin result: خانه
|
||||
2026-05-29 21:04:48 | INFO | user | User fetched items: 3 items
|
||||
2026-05-29 21:04:56 | INFO | user | Spin result: خانه
|
||||
2026-05-29 21:05:32 | INFO | user | User fetched items: 3 items
|
||||
2026-05-29 21:06:04 | INFO | user | Spin result: زمين
|
||||
2026-05-29 21:20:30 | INFO | admin | Item deleted: 2
|
||||
2026-05-29 21:20:30 | INFO | admin | Items fetched: 2 items
|
||||
2026-05-29 21:20:33 | INFO | admin | Item deleted: 3
|
||||
2026-05-29 21:20:33 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 21:20:34 | INFO | admin | Item deleted: 4
|
||||
2026-05-29 21:20:34 | INFO | admin | Items fetched: 0 items
|
||||
2026-05-29 21:21:07 | INFO | admin | Item created: پوچ
|
||||
2026-05-29 21:21:07 | INFO | admin | Items fetched: 1 items
|
||||
2026-05-29 21:21:18 | INFO | admin | Item created: تخفيف يک ماهه
|
||||
2026-05-29 21:21:18 | INFO | admin | Items fetched: 2 items
|
||||
2026-05-29 21:21:47 | INFO | admin | Item created: تخفيف يک ساله
|
||||
2026-05-29 21:21:47 | INFO | admin | Items fetched: 3 items
|
||||
2026-05-29 21:22:08 | INFO | admin | Item created: تخفيف سه ماهه
|
||||
2026-05-29 21:22:08 | INFO | admin | Items fetched: 4 items
|
||||
2026-05-29 21:22:36 | INFO | admin | Item created: 10% تخفيف يک ماهه
|
||||
2026-05-29 21:22:36 | INFO | admin | Items fetched: 5 items
|
||||
2026-05-29 21:22:44 | INFO | admin | Item created: پوچ
|
||||
2026-05-29 21:22:44 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-29 21:22:50 | INFO | user | User fetched items: 6 items
|
||||
2026-05-29 21:22:53 | INFO | user | Spin result: تخفيف سه ماهه
|
||||
2026-05-29 21:28:31 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-29 21:30:11 | INFO | user | Spin result: 10% تخفيف يک ماهه
|
||||
2026-05-29 23:55:52 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-29 23:56:28 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-29 23:56:41 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-30 00:02:00 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-30 00:02:05 | INFO | admin | Item updated: 1
|
||||
2026-05-30 00:02:05 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-30 00:02:13 | INFO | admin | Item updated: 1
|
||||
2026-05-30 00:02:13 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-30 00:02:19 | INFO | admin | Item updated: 1
|
||||
2026-05-30 00:02:19 | INFO | admin | Items fetched: 6 items
|
||||
2026-05-30 00:02:36 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 00:05:46 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 00:13:50 | INFO | user | User fetched items: 6 items
|
||||
2026-05-30 00:14:24 | INFO | user | User fetched items: 6 items
|
||||
2026-05-30 00:31:05 | INFO | user | User fetched items: 6 items
|
||||
2026-05-30 00:33:44 | INFO | user | Spin result: تخفيف يک ساله
|
||||
2026-05-30 00:43:06 | INFO | user | User fetched items: 6 items
|
||||
2026-05-30 00:43:10 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 00:45:02 | INFO | user | User fetched items: 6 items
|
||||
2026-05-30 00:45:04 | INFO | user | Spin result: 10% تخفيف يک ماهه
|
||||
2026-05-30 00:45:41 | INFO | user | Spin result: تخفيف يک ساله
|
||||
2026-05-30 00:46:22 | INFO | admin | Item created: تخفیف هفتگی
|
||||
2026-05-30 00:46:22 | INFO | admin | Items fetched: 7 items
|
||||
2026-05-30 00:46:30 | INFO | user | User fetched items: 7 items
|
||||
2026-05-30 00:47:47 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 00:51:23 | INFO | user | User fetched items: 7 items
|
||||
2026-05-30 00:51:28 | INFO | user | Spin result: تخفيف سه ماهه
|
||||
2026-05-30 00:58:22 | INFO | user | User fetched items: 7 items
|
||||
2026-05-30 01:02:03 | INFO | user | User fetched items: 7 items
|
||||
2026-05-30 01:04:14 | INFO | admin | Item created: پوچ
|
||||
2026-05-30 01:04:14 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 01:05:21 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 01:12:18 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 01:12:25 | INFO | user | Spin result: تخفيف سه ماهه
|
||||
2026-05-30 11:40:34 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 11:40:38 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 11:40:41 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 11:41:08 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:11 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:12 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:41:13 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:48:14 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 11:50:10 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:50:45 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:52:01 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:52:53 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:52:58 | INFO | user | Spin result: 10% تخفيف يک ماهه
|
||||
2026-05-30 11:54:03 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:54:47 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:56:34 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:56:38 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:56:54 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:57:12 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 11:58:04 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:02:35 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:02:45 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:03:29 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:04:32 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:05:09 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:06:05 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:06:57 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:07:43 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:10:41 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:13:15 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:13:50 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:15:50 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:16:21 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:17:21 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:17:45 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:18:16 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:19:04 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:19:18 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:19:33 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:19:54 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:21:31 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:22:30 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:23:33 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:24:29 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:24:49 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:25:02 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:26:12 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:26:23 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 12:27:13 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:27:39 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:28:21 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:28:44 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:29:09 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:29:31 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:29:55 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:30:01 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 12:30:51 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:31:19 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:31:40 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:32:00 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:32:23 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:32:54 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:33:20 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:35:04 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:35:31 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:35:48 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:36:41 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:37:39 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:38:22 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:38:45 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:39:00 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:43:07 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:43:19 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 12:45:27 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:49:55 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:50:43 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 12:51:00 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 12:53:57 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 14:30:41 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 14:31:19 | INFO | user | User fetched items: 8 items
|
||||
2026-05-30 14:31:21 | INFO | user | Spin result: تخفيف يک ماهه
|
||||
2026-05-30 14:32:54 | INFO | user | Spin result: پوچ
|
||||
2026-05-30 14:33:18 | INFO | admin | Item updated: 1
|
||||
2026-05-30 14:33:18 | INFO | admin | Items fetched: 8 items
|
||||
2026-05-30 14:33:38 | INFO | user | Spin result: تخفيف سه ماهه
|
||||
2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:26:33 | INFO | user | User fetched items: 0 items
|
||||
2026-06-07 00:32:56 | INFO | user | User fetched items: 0 items
|
||||
2026-06-07 00:36:17 | INFO | user | User fetched items: 0 items
|
||||
2026-06-07 00:36:21 | INFO | user | User fetched items: 0 items
|
||||
2026-06-07 00:39:11 | INFO | user | User fetched items: 0 items
|
||||
2026-06-07 00:39:15 | INFO | admin | Items fetched: 0 items
|
||||
2026-06-07 00:39:33 | INFO | admin | Items fetched: 0 items
|
||||
2026-06-07 00:49:16 | INFO | admin | Items fetched: 0 items
|
||||
2026-06-07 00:49:39 | INFO | admin | Item created: jotdt 20
|
||||
2026-06-07 00:49:39 | INFO | admin | Items fetched: 1 items
|
||||
2026-06-07 00:49:44 | INFO | user | User fetched items: 1 items
|
||||
2026-06-07 00:49:47 | INFO | user | Spin result: jotdt 20
|
||||
2026-06-07 00:50:31 | INFO | admin | Item created: پوچ
|
||||
2026-06-07 00:50:31 | INFO | admin | Items fetched: 2 items
|
||||
2026-06-07 00:50:38 | INFO | user | User fetched items: 2 items
|
||||
2026-06-07 00:50:40 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 00:51:12 | INFO | admin | Item created: تخفيف 20
|
||||
2026-06-07 00:51:12 | INFO | admin | Items fetched: 3 items
|
||||
2026-06-07 00:51:16 | INFO | user | User fetched items: 3 items
|
||||
2026-06-07 00:52:18 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 00:53:54 | INFO | admin | Item updated: 1
|
||||
2026-06-07 00:53:54 | INFO | admin | Items fetched: 3 items
|
||||
2026-06-07 00:54:22 | INFO | admin | Item created: تخفيف 30
|
||||
2026-06-07 00:54:22 | INFO | admin | Items fetched: 4 items
|
||||
2026-06-07 00:54:35 | INFO | user | User fetched items: 4 items
|
||||
2026-06-07 00:54:38 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 00:55:39 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-07 00:56:16 | INFO | admin | Item created: تخفيف 30
|
||||
2026-06-07 00:56:16 | INFO | admin | Items fetched: 5 items
|
||||
2026-06-07 00:56:51 | INFO | admin | Item created: پوچ
|
||||
2026-06-07 00:56:51 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-07 00:56:58 | INFO | user | User fetched items: 6 items
|
||||
2026-06-07 00:57:01 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 00:57:13 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 01:03:09 | INFO | user | User fetched items: 6 items
|
||||
2026-06-07 01:03:12 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 01:07:35 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-07 01:07:37 | INFO | user | User fetched items: 6 items
|
||||
2026-06-07 01:07:39 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-07 02:15:44 | INFO | user | User fetched items: 6 items
|
||||
2026-06-07 02:15:45 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 02:19:11 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 02:19:40 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 13:34:50 | INFO | user | User fetched items: 6 items
|
||||
2026-06-07 13:34:52 | INFO | user | Spin result: پوچ
|
||||
2026-06-07 13:35:15 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-07 13:35:36 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 11:55:45 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 11:56:01 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 11:57:45 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 11:59:13 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 11:59:33 | INFO | admin | Item updated: 3
|
||||
2026-06-10 11:59:33 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 11:59:36 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 11:59:45 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:01:01 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:02:20 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:07:31 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:08:44 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:08:47 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:17:29 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:17:32 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:18:03 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:18:44 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:19:16 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:19:20 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:20:37 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:20:41 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:20:49 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 12:22:37 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:22:39 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:22:49 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:23:08 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:23:09 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:25:32 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:25:34 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:30:55 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:30:56 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 12:36:33 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:36:35 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 12:37:57 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:48:20 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:51:46 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:58:36 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 12:58:41 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-10 12:59:01 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 12:59:04 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:00:57 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:01:44 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:01:46 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:07:23 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:07:25 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:07:51 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:09:29 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:12:47 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:14:30 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:14:34 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:15:38 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:15:41 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:16:57 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:18:40 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:19:19 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:19:21 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:19:25 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:22:09 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:22:17 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:22:20 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:23:23 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:24:44 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:25:17 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:27:10 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:28:35 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:29:16 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:29:41 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:30:47 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:31:14 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:31:41 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:32:39 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:33:20 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:35:04 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:37:57 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:39:21 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:39:41 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:39:53 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:40:34 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:42:08 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:42:11 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-10 13:44:29 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:48:32 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:48:38 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:49:14 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:49:22 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:49:32 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 13:50:41 | INFO | admin | Item updated: 3
|
||||
2026-06-10 13:50:41 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 13:51:15 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-10 13:51:53 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:53:38 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:53:58 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:55:11 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:57:25 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 13:58:19 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 13:58:48 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:02:41 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:02:44 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:03:24 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:04:02 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:04:47 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 14:06:37 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 14:07:42 | INFO | admin | Item updated: 3
|
||||
2026-06-10 14:07:42 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:07:48 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:08:03 | INFO | user | Spin result: پوچ
|
||||
2026-06-10 14:08:15 | INFO | admin | Item deleted: 6
|
||||
2026-06-10 14:08:15 | INFO | admin | Items fetched: 5 items
|
||||
2026-06-10 14:08:25 | INFO | admin | Item updated: 2
|
||||
2026-06-10 14:08:25 | INFO | admin | Items fetched: 5 items
|
||||
2026-06-10 14:08:30 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:08:38 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:08:47 | INFO | user | User fetched items: 5 items
|
||||
2026-06-10 14:08:50 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:09:40 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:11:30 | INFO | user | User fetched items: 5 items
|
||||
2026-06-10 14:12:04 | INFO | admin | Item created: تخفيف 20
|
||||
2026-06-10 14:12:04 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:12:09 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:12:16 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:13:32 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:13:34 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 14:13:54 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:14:26 | INFO | user | Spin result: 1000 لایو کوین
|
||||
2026-06-10 14:15:30 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:15:39 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:16:38 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:16:44 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:17:30 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:21:12 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 14:21:20 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-10 14:21:30 | INFO | user | Spin result: تخفيف 20
|
||||
2026-06-10 14:43:11 | INFO | admin | Items fetched: 6 items
|
||||
2026-06-10 14:45:05 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:45:40 | INFO | user | User fetched items: 6 items
|
||||
2026-06-10 14:46:11 | INFO | user | Spin result: تخفيف 30
|
||||
2026-06-10 14:46:52 | INFO | user | User fetched items: 6 items
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize
|
||||
[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability
|
||||
FROM items]
|
||||
(Background on this error at: https://sqlalche.me/e/20/e3q8)
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.db.session import engine, Base
|
||||
from app.db.models import Item
|
||||
|
||||
from api.admin import router as admin_router
|
||||
from api.user import router as user_router
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ✅ بعد router ها
|
||||
app.include_router(user_router, prefix="/user")
|
||||
app.include_router(admin_router, prefix="/admin")
|
||||
app.include_router(user_router, prefix="/user")
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -1,11 +1,27 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class ItemCreate(BaseModel):
|
||||
title: str
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str = "پوچ"
|
||||
probability: float = 1.0
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
title: str
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str = "پوچ"
|
||||
probability: float = 1.0
|
||||
|
||||
class ItemResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
prize: str
|
||||
description: Optional[str] = ""
|
||||
prize_type: str
|
||||
probability: float
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Binary file not shown.
@@ -1,61 +1,45 @@
|
||||
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
|
||||
):
|
||||
|
||||
def create_item(db: Session, title: str, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
|
||||
item = Item(
|
||||
title=title
|
||||
title=title,
|
||||
prize=prize,
|
||||
description=description,
|
||||
prize_type=prize_type,
|
||||
probability=probability
|
||||
)
|
||||
|
||||
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, prize: str, description: str = "", prize_type: str = "پوچ", probability: float = 1.0):
|
||||
item = db.query(Item).filter(Item.id == item_id).first()
|
||||
if item:
|
||||
|
||||
db.delete(item)
|
||||
|
||||
item.title = title
|
||||
item.prize = prize
|
||||
item.description = description
|
||||
item.prize_type = prize_type
|
||||
item.probability = probability
|
||||
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)
|
||||
weights = [item.probability for item in items]
|
||||
return random.choices(items, weights=weights, k=1)[0]
|
||||
Binary file not shown.
@@ -2,19 +2,160 @@
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>پنل ادمین</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>پنل مدیریت</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>مدیریت آیتمها</h1>
|
||||
<body id="main-body">
|
||||
<div class="admin-container">
|
||||
<div class="admin-header">
|
||||
<h1>مدیریت گردونه شانس</h1>
|
||||
<p>مدیریت آیتمها و جوایز گردونه</p>
|
||||
</div>
|
||||
|
||||
<div id="add-section">
|
||||
<input type="text" id="new-item" placeholder="نام آیتم جدید">
|
||||
<button id="add-btn">اضافه کن</button>
|
||||
<!-- تنظیمات رنگ پسزمینه -->
|
||||
<div class="card">
|
||||
<h2>تنظیمات ظاهری</h2>
|
||||
<div class="color-section">
|
||||
<label>رنگ پسزمینه:</label>
|
||||
<div class="color-presets">
|
||||
<div class="preset" style="background:#7B241C" data-color="#7B241C"></div>
|
||||
<div class="preset" style="background:#1a237e" data-color="#1a237e"></div>
|
||||
<div class="preset" style="background:#1b5e20" data-color="#1b5e20"></div>
|
||||
<div class="preset" style="background:#4a148c" data-color="#4a148c"></div>
|
||||
<div class="preset" style="background:#e65100" data-color="#e65100"></div>
|
||||
<div class="preset" style="background:#006064" data-color="#006064"></div>
|
||||
<div class="preset" style="background:#212121" data-color="#212121"></div>
|
||||
<div class="preset" style="background:#880e4f" data-color="#880e4f"></div>
|
||||
</div>
|
||||
<div class="color-custom">
|
||||
<label>رنگ دلخواه:</label>
|
||||
<input type="color" id="bg-color-picker" value="#7B241C">
|
||||
<button id="apply-color">اعمال</button>
|
||||
</div>
|
||||
<div class="color-preview">
|
||||
<span>پیشنمایش:</span>
|
||||
<div id="color-preview-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- عنوان صفحه گردونه -->
|
||||
<div class="title-section">
|
||||
<h3>عنوان صفحه گردونه</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>متن عنوان:</label>
|
||||
<input type="text" id="title-text-input" placeholder="گردونه شانس">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>رنگ عنوان:</label>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<input type="color" id="title-color-picker" value="#FFD700">
|
||||
<span id="title-preview" style="font-size:20px; font-weight:bold; color:#FFD700;">گردونه شانس</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<button id="apply-title" class="btn-primary">اعمال</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- افزودن آیتم -->
|
||||
<div class="card">
|
||||
<h2>افزودن آیتم جدید</h2>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>نوع جایزه:</label>
|
||||
<select id="new-prize-type">
|
||||
<option value="پوچ">پوچ</option>
|
||||
<option value="کد تخفیف">کد تخفیف</option>
|
||||
<option value="لایوکوین">لایوکوین (LC)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>عنوان:</label>
|
||||
<input type="text" id="new-title" placeholder="مثلاً: تخفیف ۲۰٪">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>مقدار جایزه:</label>
|
||||
<input type="text" id="new-prize" placeholder="مثلاً: DISC20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>احتمال (۱ = عادی):</label>
|
||||
<input type="number" id="new-probability" value="1" min="0.1" step="0.1">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>توضیحات:</label>
|
||||
<textarea id="new-description" placeholder="توضیحاتی که بعد از برنده شدن نمایش داده میشه..."></textarea>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<button id="add-btn" class="btn-primary">افزودن آیتم</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- جدول -->
|
||||
<div class="card">
|
||||
<h2>لیست آیتمها</h2>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>نوع جایزه</th>
|
||||
<th>عنوان</th>
|
||||
<th>مقدار</th>
|
||||
<th>احتمال</th>
|
||||
<th>توضیحات</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="items-list"></ul>
|
||||
<!-- مودال ویرایش -->
|
||||
<div id="edit-modal" class="modal hidden">
|
||||
<div class="modal-content admin-modal">
|
||||
<h2>ویرایش آیتم</h2>
|
||||
<input type="hidden" id="edit-id">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>نوع جایزه:</label>
|
||||
<select id="edit-prize-type">
|
||||
<option value="پوچ">پوچ</option>
|
||||
<option value="کد تخفیف">کد تخفیف</option>
|
||||
<option value="لایوکوین">لایوکوین (LC)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>عنوان:</label>
|
||||
<input type="text" id="edit-title">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>مقدار جایزه:</label>
|
||||
<input type="text" id="edit-prize">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>احتمال:</label>
|
||||
<input type="number" id="edit-probability" min="0.1" step="0.1">
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label>توضیحات:</label>
|
||||
<textarea id="edit-description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="save-edit" class="btn-primary">ذخیره</button>
|
||||
<button id="cancel-edit" class="btn-secondary">انصراف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="assets/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,61 +1,493 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@font-face {
|
||||
font-family: 'Dana';
|
||||
src: url('../fonts/DanaFaNum-Regular.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Dana' !important;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
WHEEL PAGE
|
||||
========================= */
|
||||
body {
|
||||
font-family: Tahoma, sans-serif;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(ellipse at center, #9B241CDD 0%, #7B241C 55%, #7B241CBB 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
padding: 40px 20px;
|
||||
gap: 20px;
|
||||
gap: 28px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
.page h1 {
|
||||
color: #e9e3c0;
|
||||
font-size: 42px;
|
||||
font-weight: bold;
|
||||
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: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 30px;
|
||||
font-size: 18px;
|
||||
font-family: Tahoma, sans-serif;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
.wheel-wrapper canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#spin-btn {
|
||||
padding: 18px 56px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #e7d78a, #f2dd9d);
|
||||
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;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
min-height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ادمین */
|
||||
#add-section {
|
||||
.winner-text {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
text-shadow: 0 0 20px rgba(255,215,0,0.7);
|
||||
animation: pop 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
70% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* =========================
|
||||
مودال برنده
|
||||
========================= */
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
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;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 10px;
|
||||
.admin-header h1 {
|
||||
color: #e9e8df;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 15px rgba(255,215,0,0.5);
|
||||
}
|
||||
|
||||
.admin-header p {
|
||||
color: #f5d6a0;
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(0,0,0,0.25);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #efede1;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.color-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.color-section label {
|
||||
color: #f5d6a0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.color-presets {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.preset:hover { transform: scale(1.15); }
|
||||
.preset.active { border-color: #e5e3d5; }
|
||||
|
||||
.color-custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #f5d6a0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#color-preview-box {
|
||||
width: 120px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 50px;
|
||||
height: 38px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#apply-color {
|
||||
padding: 10px 20px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
#apply-color:hover { background: rgba(255,255,255,0.25); }
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group.full-width { grid-column: 1 / -1; }
|
||||
|
||||
.form-group label {
|
||||
font-size: 15px;
|
||||
color: #f5d6a0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: white;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder {
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #e4e2d7;
|
||||
}
|
||||
|
||||
.form-group select option {
|
||||
background: #4a0f0f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-group textarea { height: 90px; resize: vertical; }
|
||||
|
||||
.btn-primary {
|
||||
padding: 13px 32px;
|
||||
background: linear-gradient(135deg, #e3dfb7, #ebe5be);
|
||||
color: #4a0f0f;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover { transform: translateY(-2px); }
|
||||
|
||||
.btn-secondary {
|
||||
padding: 13px 32px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover { background: rgba(255,255,255,0.2); }
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
background: linear-gradient(135deg, #ebe3b9, #ebe1bc);
|
||||
color: #0f374a;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 14px 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
color: white;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
tbody tr:hover { background: rgba(255,255,255,0.05); }
|
||||
|
||||
.badge {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-pooch { background: rgba(255,255,255,0.1); color: #ccc; }
|
||||
.badge-discount { background: rgba(46,125,50,0.3); color: #81C784; }
|
||||
.badge-coin { background: rgba(245,127,23,0.3); color: #FFD700; }
|
||||
|
||||
.edit-btn {
|
||||
padding: 7px 16px;
|
||||
background: linear-gradient(135deg, #9B59B6, #7D3C98);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-left: 6px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 7px 16px;
|
||||
background: linear-gradient(135deg, #E74C3C, #922B21);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover, .delete-btn:hover { transform: scale(1.05); }
|
||||
|
||||
.modal-content.admin-modal {
|
||||
background: #2d0000;
|
||||
color: white;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content.admin-modal h2 {
|
||||
color: #FFD700;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.wheel-wrapper { width: 340px; height: 340px; }
|
||||
.page h1 { font-size: 28px; }
|
||||
#spin-btn { font-size: 18px; padding: 14px 40px; }
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.title-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.title-section h3 {
|
||||
color: #f5d6a0;
|
||||
font-size: 15px;
|
||||
margin-bottom: 14px;
|
||||
font-family: 'Dana', Tahoma, sans-serif;
|
||||
}
|
||||
BIN
frontend/assets/fonts/DanaFaNum-Regular.ttf
Normal file
BIN
frontend/assets/fonts/DanaFaNum-Regular.ttf
Normal file
Binary file not shown.
@@ -1,33 +1,158 @@
|
||||
import { adminGetItems, createItem, deleteItem } from './api.js';
|
||||
import { adminGetItems, createItem, deleteItem, updateItem, saveBgColor, getBgColor, saveTitleSettings, getTitleSettings } from './api.js';
|
||||
|
||||
const list = document.getElementById('items-list');
|
||||
const input = document.getElementById('new-item');
|
||||
const tableBody = document.getElementById('items-table-body');
|
||||
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');
|
||||
|
||||
async function renderItems() {
|
||||
const items = await adminGetItems();
|
||||
list.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `
|
||||
<span>${item.title}</span>
|
||||
<button onclick="removeItem(${item.id})">حذف</button>
|
||||
`;
|
||||
list.appendChild(li);
|
||||
// ═══ رنگ پسزمینه ═══
|
||||
function initColor() {
|
||||
const saved = getBgColor();
|
||||
colorPicker.value = saved;
|
||||
previewBox.style.background = saved;
|
||||
presets.forEach(p => {
|
||||
if (p.dataset.color === saved) p.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
presets.forEach(p => {
|
||||
p.addEventListener('click', () => {
|
||||
presets.forEach(x => x.classList.remove('active'));
|
||||
p.classList.add('active');
|
||||
colorPicker.value = p.dataset.color;
|
||||
previewBox.style.background = p.dataset.color;
|
||||
});
|
||||
});
|
||||
|
||||
colorPicker.addEventListener('input', () => {
|
||||
previewBox.style.background = colorPicker.value;
|
||||
presets.forEach(x => x.classList.remove('active'));
|
||||
});
|
||||
|
||||
applyColor.addEventListener('click', () => {
|
||||
saveBgColor(colorPicker.value);
|
||||
applyColor.textContent = '✓ ذخیره شد';
|
||||
setTimeout(() => applyColor.textContent = 'اعمال', 2000);
|
||||
});
|
||||
|
||||
// ═══ عنوان صفحه گردونه ═══
|
||||
function initTitle() {
|
||||
const settings = getTitleSettings();
|
||||
document.getElementById('title-text-input').value = settings.text;
|
||||
document.getElementById('title-color-picker').value = settings.color;
|
||||
document.getElementById('title-preview').textContent = settings.text;
|
||||
document.getElementById('title-preview').style.color = settings.color;
|
||||
}
|
||||
|
||||
document.getElementById('title-text-input').addEventListener('input', (e) => {
|
||||
document.getElementById('title-preview').textContent = e.target.value || 'گردونه شانس';
|
||||
});
|
||||
|
||||
document.getElementById('title-color-picker').addEventListener('input', (e) => {
|
||||
document.getElementById('title-preview').style.color = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('apply-title').addEventListener('click', () => {
|
||||
const text = document.getElementById('title-text-input').value.trim() || 'گردونه شانس';
|
||||
const color = document.getElementById('title-color-picker').value;
|
||||
saveTitleSettings(text, color);
|
||||
document.getElementById('apply-title').textContent = '✓ ذخیره شد';
|
||||
setTimeout(() => document.getElementById('apply-title').textContent = 'اعمال', 2000);
|
||||
});
|
||||
|
||||
// ═══ badge نوع جایزه ═══
|
||||
function prizeBadge(type) {
|
||||
const map = {
|
||||
'پوچ': ['badge-pooch', 'پوچ'],
|
||||
'کد تخفیف': ['badge-discount', 'کد تخفیف'],
|
||||
'لایوکوین': ['badge-coin', 'لایوکوین (LC)'],
|
||||
};
|
||||
const [cls, label] = map[type] || ['badge-pooch', type];
|
||||
return `<span class="badge ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
// ═══ رندر جدول ═══
|
||||
async function renderItems() {
|
||||
const items = await adminGetItems();
|
||||
tableBody.innerHTML = '';
|
||||
items.forEach((item, idx) => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${idx + 1}</td>
|
||||
<td>${prizeBadge(item.prize_type)}</td>
|
||||
<td>${item.title}</td>
|
||||
<td>${item.prize}</td>
|
||||
<td>${item.probability}</td>
|
||||
<td>${item.description || '—'}</td>
|
||||
<td>
|
||||
<button class="edit-btn" onclick="openEdit(${item.id})">✏️ ویرایش</button>
|
||||
<button class="delete-btn" onclick="removeItem(${item.id})">🗑 حذف</button>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
window._items = items;
|
||||
}
|
||||
|
||||
// ═══ افزودن آیتم ═══
|
||||
addBtn.onclick = async () => {
|
||||
const data = {
|
||||
prize_type: document.getElementById('new-prize-type').value,
|
||||
title: document.getElementById('new-title').value.trim(),
|
||||
prize: document.getElementById('new-prize').value.trim(),
|
||||
probability: parseFloat(document.getElementById('new-probability').value),
|
||||
description: document.getElementById('new-description').value.trim(),
|
||||
};
|
||||
if (!data.title || !data.prize) return alert('عنوان و مقدار جایزه رو پر کن');
|
||||
await createItem(data);
|
||||
document.getElementById('new-title').value = '';
|
||||
document.getElementById('new-prize').value = '';
|
||||
document.getElementById('new-description').value = '';
|
||||
document.getElementById('new-probability').value = '1';
|
||||
renderItems();
|
||||
};
|
||||
|
||||
// ═══ حذف ═══
|
||||
window.removeItem = async (id) => {
|
||||
if (!confirm('آیا مطمئنی؟')) return;
|
||||
await deleteItem(id);
|
||||
renderItems();
|
||||
};
|
||||
|
||||
addBtn.onclick = async () => {
|
||||
const title = input.value.trim();
|
||||
if (!title) return;
|
||||
await createItem(title);
|
||||
input.value = '';
|
||||
// ═══ ویرایش ═══
|
||||
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');
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
initColor();
|
||||
initTitle();
|
||||
renderItems();
|
||||
@@ -1,20 +1,74 @@
|
||||
const BASE = 'http://127.0.0.1:8000';
|
||||
|
||||
// ---------------- USER ----------------
|
||||
export const getItems = () =>
|
||||
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());
|
||||
|
||||
export const createItem = (title) =>
|
||||
export const createItem = (data) =>
|
||||
fetch(`${BASE}/admin/items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title })
|
||||
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({
|
||||
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 => r.json());
|
||||
fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(async (r) => {
|
||||
if (!r.ok && r.status !== 204) throw new Error('Delete failed');
|
||||
return {};
|
||||
});
|
||||
|
||||
// ---------------- رنگ پسزمینه ----------------
|
||||
export const getBgColor = () =>
|
||||
localStorage.getItem('wheelBgColor') || '#7B241C';
|
||||
|
||||
export const saveBgColor = (color) => {
|
||||
localStorage.setItem('wheelBgColor', color);
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'wheelBgColor',
|
||||
newValue: color
|
||||
}));
|
||||
};
|
||||
|
||||
// ---------------- عنوان صفحه گردونه ----------------
|
||||
export const getTitleSettings = () => {
|
||||
const saved = localStorage.getItem('wheelTitleSettings');
|
||||
return saved ? JSON.parse(saved) : { text: 'گردونه شانس', color: '#FFD700' };
|
||||
};
|
||||
|
||||
export const saveTitleSettings = (text, color) => {
|
||||
const settings = { text, color };
|
||||
localStorage.setItem('wheelTitleSettings', JSON.stringify(settings));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'wheelTitleSettings',
|
||||
newValue: JSON.stringify(settings)
|
||||
}));
|
||||
};
|
||||
@@ -1,46 +1,374 @@
|
||||
import { getItems, spinWheel } from './api.js';
|
||||
import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
|
||||
|
||||
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 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;
|
||||
|
||||
async function init() {
|
||||
items = await getItems();
|
||||
drawWheel(angle);
|
||||
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 drawWheel(rotation) {
|
||||
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(() => {
|
||||
lampOn = !lampOn;
|
||||
draw(angle);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
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(255,215,0,0.3)');
|
||||
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.textAlign = 'center';
|
||||
ctx.fillStyle = theme.text;
|
||||
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) / 1.4, -9);
|
||||
ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9);
|
||||
} else {
|
||||
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';
|
||||
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();
|
||||
}
|
||||
|
||||
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 () => {
|
||||
@@ -53,26 +381,39 @@ 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;
|
||||
let lastTickIdx = 0;
|
||||
|
||||
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);
|
||||
|
||||
const currentIdx = Math.floor(Math.abs(angle) / slice);
|
||||
if (currentIdx !== lastTickIdx) {
|
||||
lastTickIdx = currentIdx;
|
||||
playTickSound();
|
||||
}
|
||||
|
||||
draw(angle);
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
spinning = false;
|
||||
btn.disabled = false;
|
||||
result.textContent = `🎉 ${winner.title}`;
|
||||
draw(angle);
|
||||
playWinSound();
|
||||
showWinnerModal(winner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,34 @@
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>گردونه شانس</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>گردونه شانس</h1>
|
||||
<canvas id="wheel" width="400" height="400"></canvas>
|
||||
<br>
|
||||
<button id="spin-btn">بچرخون!</button>
|
||||
<p id="result"></p>
|
||||
<body id="main-body">
|
||||
<div class="bg-rays"></div>
|
||||
<div class="page">
|
||||
<h1> گردونه شانس</h1>
|
||||
<div class="wheel-wrapper">
|
||||
<canvas id="wheel" width="500" height="500"></canvas>
|
||||
</div>
|
||||
<button id="spin-btn">چرخوندن!</button>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<!-- مودال برنده -->
|
||||
<div id="winner-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-icon" id="modal-icon">🎉</div>
|
||||
|
||||
<div id="modal-prize-type"></div>
|
||||
<div id="modal-prize"></div>
|
||||
<div id="modal-description"></div>
|
||||
<button id="modal-close">باشه!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="assets/js/wheel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user