From ec86cdc528631ddc1ac18719e16df365241ec837 Mon Sep 17 00:00:00 2001 From: Anahita-Mahmoudi Date: Wed, 10 Jun 2026 14:55:07 +0330 Subject: [PATCH] feat: implement advanced reward management and wheel customization - Add customizable background color - Add wheel animations and sound effects - Add reward description field and winner modal - Change font to Dana - Add configurable reward probabilities - Support reward types (Empty, Discount Code, LiveCoin) - Replace reward title with reward type selector - Allow wheel page title customization from admin panel --- backend/__pycache__/main.cpython-313.pyc | Bin 939 -> 928 bytes backend/__pycache__/schemas.cpython-313.pyc | Bin 1151 -> 1533 bytes ...14a1411_update_items_table.cpython-313.pyc | Bin 0 -> 1001 bytes ...2bbdfc6_update_items_table.cpython-313.pyc | Bin 0 -> 1001 bytes .../eb35214a1411_update_items_table.py | 28 + .../f71dc2bbdfc6_update_items_table.py | 28 + backend/api/__pycache__/admin.cpython-313.pyc | Bin 4101 -> 4333 bytes backend/api/__pycache__/user.cpython-313.pyc | Bin 2509 -> 2672 bytes backend/api/admin.py | 77 +- backend/api/user.py | 5 +- .../app/db/__pycache__/models.cpython-313.pyc | Bin 711 -> 858 bytes backend/app/db/models.py | 28 +- backend/logs/app.log | 134 ++++ backend/logs/error.log | 32 + backend/main.py | 15 +- backend/schemas.py | 19 +- .../services/__pycache__/crud.cpython-313.pyc | Bin 2212 -> 2657 bytes backend/services/crud.py | 20 +- backend/spinwheel.db | Bin 12288 -> 12288 bytes frontend/admin.html | 207 ++++-- frontend/assets/css/style.css | 666 +++++++++++------- frontend/assets/fonts/DanaFaNum-Regular.ttf | Bin 0 -> 87024 bytes frontend/assets/js/admin.js | 223 +++--- frontend/assets/js/api.js | 76 +- frontend/assets/js/wheel.js | 117 ++- frontend/index.html | 24 +- 26 files changed, 1166 insertions(+), 533 deletions(-) create mode 100644 backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc create mode 100644 backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc create mode 100644 backend/alembic/versions/eb35214a1411_update_items_table.py create mode 100644 backend/alembic/versions/f71dc2bbdfc6_update_items_table.py create mode 100644 frontend/assets/fonts/DanaFaNum-Regular.ttf diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 8048de56923949746a95a9238e4659d097c655c1..1504b453673fb34e792cab0611fb7781bc7876a1 100644 GIT binary patch delta 434 zcmZ3@zJQ(gGcPX}0}!yOYGzHH$ScY8k71&Qnw1w@5t{--FheklH@g={5eJyf>dooJ zRm2Tt^8ner3JfufN(`nT6M&$IFNSa81krltV76d(eWqZJU`~BTFy^vk1uEqWQiLc( zCS!Pmxr2EunQ+Q;2lGPZ*@Bdibwb!ds_0^T3=A=>!F-mC3JmG|ngWxJ7?nAhZn352 zrDx`)PEKHyoIHuKM1fVmv^cd$lkpZ?K~ZX2W`IjQA|MX8gQFsV(x#bl-43$*+LGb1D8T?Xm93`Wlx)bBFL zJ!R0j%b@j;jkh_of%yX)10&1i6lOg~2_WM-gV03=p$XOt$}fOe`ZpMup+Yt*IKV=N e4;Wb5c^i4JGe}%ykeHEnfkEODgKCi+&>{dxbXr*e delta 483 zcmZ`z&r1S982x5{WPFSG)Ret+!M-s{tXbMEAs%?;moR?MX3TBV`g2O z7R#Wqa;!Jpe%fR;@y7hF)nxp^qqQ20J4Btcs(J5NuT0CZE!0x6rW-ZWvSsO9k@b4r zzLXB^zLXA+L%LyH)J(&;Wo?}^dmtU2y@}l4GD2tq>l4_U!Nvp^m`M zM?WQ6uaVecHnWv^Hm86xPIPWQH>ZL+&IZ_ZW~)QY)*NL<&3r(UQXQp6n^c$5JW9#sg%`#W?k&&6z<%b$N3qAGI zfD1ha|2-E>6l*-Gf-i4*VxYzT*Zq%TOIXstP1j@o1EqM59*SZN(XzMf2Y$M~%NBM^ z!gSrxYjD>Umg_d6+FC$#$#vhYdBLfs%XL?LNYcO$c^IKuO;Qjj^};YpJ?c;5x=_R% zKzD==f-VH1uLO~oiays($gt{qX$t=GTFS|z8%u1{0GT^VEs$DlUg~oP66#qlF7X);9glXxW#n9 zvWqyyrAmh=(G)-DNsN=i53it7_71U sZlR}wPjVaKNL3W&fRzu}@Uf;VquYbW3}45c?Q}sI+rE3k@Fj2a3wO&p4FCWD literal 1151 zcmb_bO>fgc5Z(1xoR2m@sc3`L77&6U)C=kXgg8(kAc!7n5En};H`$~H#|~o$spm-L zz@?Y|i2ezFN3{|sPPrjDh$}PeL?oyO4y@$2GjG;A<2U1cX{o|+J@~r*{w`stWhVQ( zAcIVfEet3-3$sWn`%@4Ni_}-iy*2~=#wvNbF5(yJqHmy;zd-nx?&*$N)JHurHcpJQW6%|tqvVyqrI}GR z>g~Qv2dKRqFJn*{Vdqcn3q{~a>y@wJwpfH`faRHB_pGL^R^~6=-c90e*c;P#y(y$< zI#5Lqs3)*QpcB`-ko4#J4Ob|nmC>S*V#+D5gAH_uO-bfscHY?dQZKZ-fy6XV`Jy~< znGRecs1xY(7o>s)MG2_7^g03#kXs!+pH0!I^~>V+=7b>>x#{#CD&P)|Izf!%3-sF+ zqo{kW@BHhN+{eWa^8{|;VG=pLkMP6foPT2*6UX57ll2M1ZBm)H9lm+;@F&AfXa5Z> Ch2cT~ diff --git a/backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc b/backend/alembic/versions/__pycache__/eb35214a1411_update_items_table.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab06778eeb06c873a075a207d8e6d651dafb7015 GIT binary patch literal 1001 zcmah|&1(}u6rcS_ve{;nR;@x2yHG@KVLzs6>SmL^-BeQ$ za_q4l3VQ2NZ~iGQy_E4HhzD=QDqfx0#8SoLu=AVu_V@OE{DwW%G!^mnYtkYO8jKJlJGL*F5t5xUb}!-6JE2%rqG5Ac}uf$iB)LI#r$@=*Y%y4=&hKv zBRzK9fT-#dvek-Oolsw#Z|KCW)J&u57*)d8DHO%+!fLk( zj!*Q+TPJO&wDKN-#$(tBK-zB0v$qIvks|wi(|D*S zPV~fyo~3S|W_rUyFwPQB@(TQR2k4WmK%QCu?Q~+E&HQJtNvA{LDWJfBX1O*=99(Ap z6elJ)@DQea8c5uOaHTj-GZ_0F4`Z5+`)tZ4aCLyILo=fpfp2&_|-&x1}Bmr2;aoC zApguGVde|E`Vn0_QP9NnK-rl&LN|_$xq-IR+qK@@-mko!eRt(xa`v!LIzn^DM(y8B n?YLwP)SZRhncbKB((70I{=ua?hq=2)$o!rbh3pTNK_bsD&k6b( literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc b/backend/alembic/versions/__pycache__/f71dc2bbdfc6_update_items_table.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2593d94248bd21910a1c6e7d3bb3dcdcf2229298 GIT binary patch literal 1001 zcmah|&1(}u6rcS_ve{;nR;@x2yHG@7|SpK|FXXR`Kf0CY34{hn=_Y&F}4dzxNyVOw&}v)-!W@vnC?+ znFr@f9ywc9mggk?X1EgLGGs2n(<5BF1Xp8RGsdRSrVLp_GcvK4YsvZSPN&y(t%&ID zh;%|dvTTp2>Jzfv4%>mRFE2Lq^?J#5%C_yUJGCJ~!iH{FtMkQqYrghiuC6`?!fPxt z)6mPsa&5NAo{C;9H%jG3rCO-ei*xn5s(xi7p}a^HgKYJP?+}_;^I4<3K*gZDN;4Om zQ3XakXy#%&s=2`nAO8k58!X@1z@B9jFQlqVy2N)w9QY1zcfJJXNNk_+E89mcAusOa z*19dQT%w202I*LZ)%OTAp2Iex%S#Q@=(c_R=>{R5X7R@k&W(y`l)xlMqJ6q*-F zJ1ygI4eBKJMh~bpii;_S?2wh>BJ(N`A@Df)1l!dAjb?%b8X^0 zxXR)w4oq;vQ<(BeAW;we)%-Y3VeAGD#xxoA*_18d>Ht@VrbaUYXGzs?%dsJTv;gx$wnfu46{5>fO=^rXHu{^&3z-0SU literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/eb35214a1411_update_items_table.py b/backend/alembic/versions/eb35214a1411_update_items_table.py new file mode 100644 index 0000000..29c1da5 --- /dev/null +++ b/backend/alembic/versions/eb35214a1411_update_items_table.py @@ -0,0 +1,28 @@ +"""update items table + +Revision ID: eb35214a1411 +Revises: f71dc2bbdfc6 +Create Date: 2026-06-03 02:15:20.584639 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eb35214a1411' +down_revision: Union[str, Sequence[str], None] = 'f71dc2bbdfc6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py b/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py new file mode 100644 index 0000000..490762c --- /dev/null +++ b/backend/alembic/versions/f71dc2bbdfc6_update_items_table.py @@ -0,0 +1,28 @@ +"""update items table + +Revision ID: f71dc2bbdfc6 +Revises: b55808a86947 +Create Date: 2026-06-03 02:12:35.370477 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f71dc2bbdfc6' +down_revision: Union[str, Sequence[str], None] = 'b55808a86947' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass diff --git a/backend/api/__pycache__/admin.cpython-313.pyc b/backend/api/__pycache__/admin.cpython-313.pyc index d8926f41b5fc54b4f4a2be9ca7f28c34d9852b95..04fb49de0a9bc7584f6c7200918a8e58929767d3 100644 GIT binary patch delta 1443 zcmZ`(UuauZ96sls`zN{g<|fTe(l%+G276LOxc7kk}U%*j~=D{d@p+=zueLv%D-*l&2F00&Gh-%Xw<`g5`w zG$nza7|L1Bz?5oLvvhdwDNuZu?k z;PsQQrHRSYBN^~8Bag<=!}IN<5wslnL-n_x!3}o>Vc?Y8v{;eKoFiQ4CHnL)ieL$lXZ5 zJ>)O-)O4CvnHiwF@)b2qdYwuijO+!IkAq29di|;FjYoKf`hJftuSyE( z4)OlTx<>BH!>yx%Q5|gSp)mn%r`pE=dISU-Z}oddU?@LxA&q5?hO{ zzqz@z8ry-HgW*>mi1++8ph4}{!d|3zH_}^W$?b(B<_;0y2CE#6s!4{8u0?m$6Sv=A zjqbp62Ny1W`_7}48VEt{Grh{(=EPplncbcY*$a6^ZRPyWwsqCvz1=XZYp|NVd5 z|L<^q&AgX34t+ii!TRd=c}b1ncEsNixDxJX5pdrF=n zp~U5+BvA=srIJ*XsqFgXl2TNu>T;!|6?Lk+T-|slIi*cQE54}T(i4c96{DOx^7NzS ztpqDF0hi@tCXNu27cBqA!;w?oifs92go=y54XC)jTyk-A$u$_?e#3SIUB4`Olub(*O09tjNDQB;O#J(daL{_$Y3x zg;!oGUurdN3Wj{vUbR=MjZwUPT*zY_*ghwAFix-+$wxX@LWF(z2Vw_gSXeNbCXGd> zxewNgu&5j{gvvU*DxFDrd$x?01|dT1i}15YQaz_op3~MZPVpFTMZk&Pfp&9n4FB9e0 zM>+62Xk6`fW0S^b$1ZdS1s_5ZY*&nz*#j~nEu_!mvWHEm5hkdip=pmB1QVYZpE#nZ znnkE@ex1Fop1?kKT@C5~=;mj))bYuF-2{Zv$VQ81w+!lP>FKK#Vo7Z#Omh%Ihd7+z z!1ZF+w5cJjD{^bK+7p?=bs&12Lzu%jd*F+S1VlKdk3V~oyEx3lZd?NZ$-M?h9*ph< zvv-5p&1beeH?l3 zHz?j7B)~@p*gfORSEZ_5x0`lbTwZCqlDEaxR@3eK1v-SL@IC?B%5xVlRaTs;D}%yDE`OXdnh6y?+C!00iK^)OWpBjc5${Z*$>G+(-Glx z)8OQ3IY$8IWO-U*Ey3P0wb_o+Ybl4iS#Tcw-I{>qNJupw^Y6UMY)1e_Ps5yl0UEdn A(*OVf diff --git a/backend/api/__pycache__/user.cpython-313.pyc b/backend/api/__pycache__/user.cpython-313.pyc index 51e0d47184544f5459d38d4a92f401b83065319f..3747bc80d4027aabdb698d298c53c3a126243c86 100644 GIT binary patch delta 415 zcmX>r{6U28GcPX}0}%8nX=cT-ZR8VS2vr2c z1B!}ZirO-2Km}8n7#I{7g4u&P^jIe=vPmq>jMJ^C)bZJB9lcp%*C9PoKzVOsfsv@ zGas^)bQWShEX3+8J~@$NnGDF9l??0G9Rb&iQ2>{y9Rqp@* delta 253 zcmew$a#on{GcPX}0}xnm6UsWpx{*(Wm5UX~Wd`EU36phLKPm*X>oFNHsxT-rM00>t zf&e>1A`=6H0z)c8w7}$vY|^5_5OHKOm}BxmHf2W6$xqqxc{Q1CF=eLQVlBxm$w{5u z%&y4DHF+t!7Mna!N0Hp*C+tQ-NG4qA&i=`tVUmvqu#KBU3wq&t~~a~Y%3n2&(mCc85Cx?1z zKqx&n1fjqR_P1h1YKFU^36$KDF+-PyH0^uc@c|GoLYdCWJJ6Z5xe8c4*$X65zK z*u1>8D&Re$h~frvFknXl$&o>J6i^n@zSK}10%QTrp(>*CJfcdI)a3LLgg^^an;fUo zRycXkkA~eI*Xljy2R?8@8HBxntB<0-$85}%H81wtsr@v@9}UdLpYPOls#3Bffzec} zsHsz#YE)m6A%Ds$&1GE3*c5If;~AQV*`Q#XT&Xj^d#p?heDelJwgfX`2iH3OOK%vl zArUnFI^E&h!T;SS_dcKv`@&=ezo-u&eCzXk$B)|(4p`XlaU<)wY;)j4I?M9|=(oI9 z7=>(;mt8kz9t+#9$HX67L*~b>3&qJ7d}}Kw?2r~kWc*x|ZFE$+GJ5Phkb$eD_}vLHFJmftnt z>e;3ZTjxgnCi0^8tI1Qzl7LJ+sq{oUGe9v_nZF_w*O$Bf&M@*dv8ucNDj`C)vZ9)*yPNZFn zw(a@^ZTkwtfKZDT?viTXn*lG}^M3d6?%@XbT~vQmRRLDzx9rvPRL$Nr*zgDj7^n#i zMA2@@9Al1h5%Y|B%110Tg@#DQUC6*57~&Qf_*^{T!$1Ivdy+0MLex=!Peuc8Xfu-P zwo7`1hNj`tzTIOaR4s>e0H#kVbUJ-K%wj_{@DWE9L!xUvS&s!m-6g|?JS(B1_yR3G zK6^lyg==fzkzm6#9oKUE9n*4IWZQFzW13W(mATS|93B%&*iMDX9ekv7le5oQ!j}_% zI^%1}r6eeLQeDPDP5H9suWv3zUd5BzQi+_z%DJu4%qp;@OS6E>bpp*AkBi-Q(IJl0 zAK4VIeo!r>Xd%lekdKF>t~VeRy3Iz!+Q~Ed<`F`3$j_m`^b2-=aT4MC{Xbw&wEhoP C0cPv~ diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 48d048c..b9c0c4d 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -1,26 +1,12 @@ -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String - +from sqlalchemy import Column, Integer, String, Float from app.db.session import Base - class Item(Base): - __tablename__ = "items" - id = Column( - Integer, - primary_key=True, - index=True - ) - - title = Column( - String, - nullable=False - ) - - value = Column( - Integer, - nullable=False - ) \ No newline at end of file + id = Column(Integer, primary_key=True, index=True) + title = Column(String, nullable=False) + prize = Column(String, nullable=False) + description = Column(String, nullable=True, default="") + prize_type = Column(String, nullable=False, default="پوچ") + probability = Column(Float, nullable=False, default=1.0) \ No newline at end of file diff --git a/backend/logs/app.log b/backend/logs/app.log index c3509ca..c77bd98 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -287,3 +287,137 @@ FROM items] 2026-05-30 12:50:43 | INFO | admin | Items fetched: 8 items 2026-05-30 12:51:00 | INFO | user | User fetched items: 8 items 2026-05-30 12:53:57 | INFO | user | Spin result: پوچ +2026-05-30 14:30:41 | INFO | admin | Items fetched: 8 items +2026-05-30 14:31:19 | INFO | user | User fetched items: 8 items +2026-05-30 14:31:21 | INFO | user | Spin result: تخفيف يک ماهه +2026-05-30 14:32:54 | INFO | user | Spin result: پوچ +2026-05-30 14:33:18 | INFO | admin | Item updated: 1 +2026-05-30 14:33:18 | INFO | admin | Items fetched: 8 items +2026-05-30 14:33:38 | INFO | user | Spin result: تخفيف سه ماهه +2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:26:33 | INFO | user | User fetched items: 0 items +2026-06-07 00:32:56 | INFO | user | User fetched items: 0 items +2026-06-07 00:36:17 | INFO | user | User fetched items: 0 items +2026-06-07 00:36:21 | INFO | user | User fetched items: 0 items +2026-06-07 00:39:11 | INFO | user | User fetched items: 0 items +2026-06-07 00:39:15 | INFO | admin | Items fetched: 0 items +2026-06-07 00:39:33 | INFO | admin | Items fetched: 0 items +2026-06-07 00:49:16 | INFO | admin | Items fetched: 0 items +2026-06-07 00:49:39 | INFO | admin | Item created: jotdt 20 +2026-06-07 00:49:39 | INFO | admin | Items fetched: 1 items +2026-06-07 00:49:44 | INFO | user | User fetched items: 1 items +2026-06-07 00:49:47 | INFO | user | Spin result: jotdt 20 +2026-06-07 00:50:31 | INFO | admin | Item created: پوچ +2026-06-07 00:50:31 | INFO | admin | Items fetched: 2 items +2026-06-07 00:50:38 | INFO | user | User fetched items: 2 items +2026-06-07 00:50:40 | INFO | user | Spin result: پوچ +2026-06-07 00:51:12 | INFO | admin | Item created: تخفيف 20 +2026-06-07 00:51:12 | INFO | admin | Items fetched: 3 items +2026-06-07 00:51:16 | INFO | user | User fetched items: 3 items +2026-06-07 00:52:18 | INFO | user | Spin result: پوچ +2026-06-07 00:53:54 | INFO | admin | Item updated: 1 +2026-06-07 00:53:54 | INFO | admin | Items fetched: 3 items +2026-06-07 00:54:22 | INFO | admin | Item created: تخفيف 30 +2026-06-07 00:54:22 | INFO | admin | Items fetched: 4 items +2026-06-07 00:54:35 | INFO | user | User fetched items: 4 items +2026-06-07 00:54:38 | INFO | user | Spin result: پوچ +2026-06-07 00:55:39 | INFO | user | Spin result: تخفيف 30 +2026-06-07 00:56:16 | INFO | admin | Item created: تخفيف 30 +2026-06-07 00:56:16 | INFO | admin | Items fetched: 5 items +2026-06-07 00:56:51 | INFO | admin | Item created: پوچ +2026-06-07 00:56:51 | INFO | admin | Items fetched: 6 items +2026-06-07 00:56:58 | INFO | user | User fetched items: 6 items +2026-06-07 00:57:01 | INFO | user | Spin result: پوچ +2026-06-07 00:57:13 | INFO | user | Spin result: پوچ +2026-06-07 01:03:09 | INFO | user | User fetched items: 6 items +2026-06-07 01:03:12 | INFO | user | Spin result: پوچ +2026-06-07 01:07:35 | INFO | admin | Items fetched: 6 items +2026-06-07 01:07:37 | INFO | user | User fetched items: 6 items +2026-06-07 01:07:39 | INFO | user | Spin result: تخفيف 20 +2026-06-07 02:15:44 | INFO | user | User fetched items: 6 items +2026-06-07 02:15:45 | INFO | user | Spin result: پوچ +2026-06-07 02:19:11 | INFO | user | Spin result: پوچ +2026-06-07 02:19:40 | INFO | user | Spin result: پوچ +2026-06-07 13:34:50 | INFO | user | User fetched items: 6 items +2026-06-07 13:34:52 | INFO | user | Spin result: پوچ +2026-06-07 13:35:15 | INFO | admin | Items fetched: 6 items +2026-06-07 13:35:36 | INFO | user | Spin result: پوچ +2026-06-10 11:55:45 | INFO | user | User fetched items: 6 items +2026-06-10 11:56:01 | INFO | admin | Items fetched: 6 items +2026-06-10 11:57:45 | INFO | user | Spin result: پوچ +2026-06-10 11:59:13 | INFO | user | Spin result: پوچ +2026-06-10 11:59:33 | INFO | admin | Item updated: 3 +2026-06-10 11:59:33 | INFO | admin | Items fetched: 6 items +2026-06-10 11:59:36 | INFO | user | Spin result: پوچ +2026-06-10 11:59:45 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:01:01 | INFO | user | Spin result: پوچ +2026-06-10 12:02:20 | INFO | user | Spin result: پوچ +2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items +2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items +2026-06-10 12:07:31 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items +2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items +2026-06-10 12:07:31 | INFO | user | User fetched items: 6 items +2026-06-10 12:08:44 | INFO | user | User fetched items: 6 items +2026-06-10 12:08:47 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:17:29 | INFO | user | User fetched items: 6 items +2026-06-10 12:17:32 | INFO | user | Spin result: پوچ +2026-06-10 12:18:03 | INFO | user | Spin result: پوچ +2026-06-10 12:18:44 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:19:16 | INFO | user | User fetched items: 6 items +2026-06-10 12:19:20 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:20:37 | INFO | user | Spin result: پوچ +2026-06-10 12:20:41 | INFO | user | User fetched items: 6 items +2026-06-10 12:20:49 | INFO | admin | Items fetched: 6 items +2026-06-10 12:22:37 | INFO | user | User fetched items: 6 items +2026-06-10 12:22:39 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:22:49 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:23:08 | INFO | user | User fetched items: 6 items +2026-06-10 12:23:09 | INFO | user | Spin result: پوچ +2026-06-10 12:25:32 | INFO | user | User fetched items: 6 items +2026-06-10 12:25:34 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:30:55 | INFO | user | User fetched items: 6 items +2026-06-10 12:30:56 | INFO | user | Spin result: پوچ +2026-06-10 12:36:33 | INFO | user | User fetched items: 6 items +2026-06-10 12:36:35 | INFO | user | Spin result: تخفيف 20 +2026-06-10 12:37:57 | INFO | user | User fetched items: 6 items +2026-06-10 12:48:20 | INFO | user | User fetched items: 6 items +2026-06-10 12:51:46 | INFO | user | User fetched items: 6 items +2026-06-10 12:58:36 | INFO | user | User fetched items: 6 items +2026-06-10 12:58:41 | INFO | user | Spin result: تخفيف 30 +2026-06-10 12:59:01 | INFO | admin | Items fetched: 6 items +2026-06-10 12:59:04 | INFO | admin | Items fetched: 6 items +2026-06-10 13:00:57 | INFO | user | User fetched items: 6 items +2026-06-10 13:01:44 | INFO | user | User fetched items: 6 items +2026-06-10 13:01:46 | INFO | user | User fetched items: 6 items diff --git a/backend/logs/error.log b/backend/logs/error.log index 9d08148..895e7b1 100644 --- a/backend/logs/error.log +++ b/backend/logs/error.log @@ -38,3 +38,35 @@ FROM items] [SQL: INSERT INTO items (title) VALUES (?)] [parameters: ('جایزه 1',)] (Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:01:07 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:01:11 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:08:13 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.type +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.type AS items_type, items.description AS items_description, items.probability AS items_probability, items.value AS items_value +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-03 02:30:22 | CRITICAL | admin | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:22:06 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:22:08 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:23:31 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +2026-06-07 00:24:02 | CRITICAL | user | Unexpected error fetching items: (sqlite3.OperationalError) no such column: items.prize +[SQL: SELECT items.id AS items_id, items.title AS items_title, items.prize AS items_prize, items.description AS items_description, items.prize_type AS items_prize_type, items.probability AS items_probability +FROM items] +(Background on this error at: https://sqlalche.me/e/20/e3q8) diff --git a/backend/main.py b/backend/main.py index fa62976..113a07c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,19 +1,14 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.db.session import engine -from app.db.session import Base - +from app.db.session import engine, Base from app.db.models import Item -app = FastAPI() - from api.admin import router as admin_router from api.user import router as user_router -app.include_router(user_router, prefix="/user") +app = FastAPI() -Base.metadata.create_all(bind=engine) app.add_middleware( CORSMiddleware, @@ -26,4 +21,8 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(admin_router, prefix="/admin") \ No newline at end of file +# ✅ بعد router ها +app.include_router(user_router, prefix="/user") +app.include_router(admin_router, prefix="/admin") + +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/backend/schemas.py b/backend/schemas.py index 45c04a2..ef9e1f3 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,20 +1,27 @@ from pydantic import BaseModel - +from typing import Optional class ItemCreate(BaseModel): title: str - value: int - + prize: str + description: Optional[str] = "" + prize_type: str = "پوچ" + probability: float = 1.0 class ItemUpdate(BaseModel): title: str - value: int - + prize: str + description: Optional[str] = "" + prize_type: str = "پوچ" + probability: float = 1.0 class ItemResponse(BaseModel): id: int title: str - value: int + prize: str + description: Optional[str] = "" + prize_type: str + probability: float class Config: from_attributes = True \ No newline at end of file diff --git a/backend/services/__pycache__/crud.cpython-313.pyc b/backend/services/__pycache__/crud.cpython-313.pyc index 3c9f16101cca35f78cde55d58ab1d2bc21b86a7e..fa52f42422dfed9a980e5dad206ed1af946bf464 100644 GIT binary patch literal 2657 zcmd5;O>7fa5Ptjbc>NQUP}0!g1p;)XDs3SY5}_d4g#5J;_148wEJcoYlWbW3@OD!a zPDVn76r|(;QjckcMCmEVo~j;us$vBxW(!<;;$|8+^wyc(wc}b)sp8U+cV^z3d4KcG zo9CTqG)#bg%On;T1w#J7MxEk;vhzJCcZo(cW|B~p>?AY6QdU8oo8%^V%7e~l*nU1+5+1nwSwa3BC|~ZSaLNQh!8~(PxpNp};GWX?vw-Op6AdVQ)6@>*uwYw}Kq3 z7x!9`c}Csy3^zQbGOnZ^(X&H9p% zlYun}A!?IwT zT`~%K#y03zX3nrPIl83xm6qMMdBd48Ve2-PVW2m|+n|(5RgSKnUO9bd_}0+2+*6Tz zw&cVnpV$E-aRtY8EW;H_)VylAvToQpYL*-bg&X$zW}M{`7)rD_o1HZ+(^<}mI5MCR zNQ}+SbzpbNbR#M$CdH<>6rU0_WawlC~q;15Aa)0|9Z?byv(4^Lf*8C2Gu3!(LD&FYvD&?DAfGaTAJ96z_s7 z=cti&3@`e0AM{{Q?2mz!$zwTQjdkF!Gg0m60sdSTBVu{D79)am+3)z+x*^UUQg+Ql#<>J4B4}Uq>ot*NYkgR#@y*qt8tR_MGNEQQF`8L%ns?Wx8A=i z>nnNbfU$0&lvLj*cu2j3;yP-C;u1de;+$zY26b6ecZE5V+RkeMr=2(occF&-)ab&F z*CiKy3#>M-6r_g|%713yL~^5Rt}!nlKZ#i!HPV% zC7&q|Kb7R_-tOCBpY1>TFw|A;?!6ta`f4b=+P2bmTdsloxBXJHw%8?{iu@*|PP2iFw;?P1(>A}{)mv+IY-QZ96BYOv!E9!=2Kr$fC zD%S+Dha_GuKr#T)cl9s10v6aMvB!Y@R}g)q))2Y|XuCi)3MkBs_r<&6zk>2+K-B|N zU{Xvy&^oL&54XF6e97AE`^08D$QTsO;-ObnfeY+sP)22EVv(>N>uy)d@|UxA&25=Qbj2{|7aZGVE>TQSRcwL^3xQX&U6E!B zdNJ=xxrHLWc~Hc0QdjVwJH(0}24-Vo17HDSVH=*!nJf5gQamz#_UFI=ut;omY-Q}u z7q_OiBL^yx16vWLJn~d-uf{tbz4zf(?D&r(>%-ge#KU;v!AN;@Gj{y3yk~Xjo1t%y zY@DmeA8lN&$OD`Fz+ZoIB!0ri?QHBlm*C+!Crb$1qn{t$bibYl4XAcjjnX5axH0>( zm9=sUMt-@kNb?lYb)(r*sZXEn%NHTXY_CdrZ@9}rZGswQ6|;`_uDG08a41#~svyG? zJs{o_;U{V649u})8AJ2~FkzJK`@m`(!!W;-=o5122{};X8Ae$<_JV+TF3L>%+Gve{ zSpUQm8=NP8IqZqe^t3ORuD(R4=iOm{0EqQmjeytyPY^%(t<4LX*W8>lJy|1Um>p+d dqD!qy@CSof&%GcZYB7NsTs!p=^g-_+e*>jO&~#vNGB~pdDG< zk(SQO*9QGr;>dYoNo7moQRw00;)66>W*pBSAdvM~qwQ{a4ALwZ$NT7+HSSj1?mALD z0SMlOR|MA~$L2)W{c2jBTi=-~%5sr1(UB9hFOHP`%gkYO(1uo>CuB9>#BqVGmYQmq zgM zxQ$?APv8!gM2AJhM=id$Hk7*(z9nc3Ek_(0Y7F%$92+?`}pcqsdCdjDUI|EOl0P26bUxT~448>uxsQ?7G>gU~FJ)VhS^;9%hmu3s9w~qBfdJ zknKRbZ?@f7f3yA8${XtyASMBw)(8waesx(@NvIY}Hi!xANOl<+Al!em>()v%6C7B~ yfQA5pI3tP&OyC{>`i2WA&&Gd`f&UNxH~tU&uYh5AZ?mGpM*fKnl9TW0D**t;V7=S` delta 337 zcmZojXh@hK%_uTa##vB=L9b1dmw|zSiC>?AznEWtv!cL$zR9=wTsgTI*~P`h85@fy zYx0M)mL=wtrcMsz7oGf$UsxQdfrH - - - پنل مدیریت - + - - - +
-
-

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

+

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

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

+ +
+

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

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

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

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

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

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

لیست آیتم‌ها

+
+ + + + + + + + + + + + + +
#نوع جایزهعنوانمقداراحتمالتوضیحاتعملیات
+
+
+ + - - - \ No newline at end of file + diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 5f262d2..301ef9b 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -1,261 +1,68 @@ -/* ========================= - ADMIN PANEL -========================= */ - -.admin-container { - width: 100%; - max-width: 1200px; - padding: 30px; +@font-face { + font-family: 'Dana'; + src: url('../fonts/DanaFaNum-Regular.ttf') format('truetype'); + font-weight: 100 900; + font-style: normal; } -.admin-header { - text-align: center; - margin-bottom: 30px; -} - -.admin-header h1 { - color: #FFD700; - font-size: 38px; - text-shadow: 0 0 15px rgba(255,215,0,0.5); -} - -.admin-header p { - color: #f5d6a0; - margin-top: 10px; - font-size: 18px; -} - -.add-section { - background: rgba(0,0,0,0.25); - backdrop-filter: blur(8px); - - border: 1px solid rgba(255,255,255,0.1); - - border-radius: 20px; - - padding: 20px; - - display: flex; - gap: 15px; - - margin-bottom: 30px; - - box-shadow: - 0 10px 30px rgba(0,0,0,0.4), - inset 0 0 20px rgba(255,255,255,0.03); -} - -.add-section input { - flex: 1; - - padding: 14px; - - border: none; - - border-radius: 12px; - - background: rgba(255,255,255,0.08); - - color: white; - - font-size: 16px; - - outline: none; -} - -.add-section input::placeholder { - color: rgba(255,255,255,0.5); -} - -#add-btn { - padding: 14px 28px; - - background: linear-gradient( - 135deg, - #FFD700, - #D4AC0D - ); - - color: #4a0f0f; - - border: none; - - border-radius: 14px; - - font-weight: bold; - - cursor: pointer; - - transition: 0.2s; - - box-shadow: - 0 4px 20px rgba(255,215,0,0.3); -} - -#add-btn:hover { - transform: translateY(-2px); - - box-shadow: - 0 8px 25px rgba(255,215,0,0.45); -} - -.table-wrapper { - background: rgba(0,0,0,0.25); - - backdrop-filter: blur(10px); - - border-radius: 22px; - - overflow: hidden; - - border: 1px solid rgba(255,255,255,0.08); - - box-shadow: - 0 10px 40px rgba(0,0,0,0.45); -} - -table { - width: 100%; - border-collapse: collapse; -} - -thead { - background: - linear-gradient( - 135deg, - #FFD700, - #D4AC0D - ); - - color: #4a0f0f; -} - -th { - padding: 18px; - font-size: 17px; -} - -td { - padding: 18px; - text-align: center; - color: white; -} - -tbody tr { - border-bottom: - 1px solid rgba(255,255,255,0.06); - - transition: 0.2s; -} - -tbody tr:hover { - background: - rgba(255,255,255,0.05); -} - -.action-buttons { - display: flex; - justify-content: center; - gap: 10px; -} - -.edit-btn, -.delete-btn { - border: none; - - padding: 10px 16px; - - border-radius: 10px; - - cursor: pointer; - - font-weight: bold; - - transition: 0.2s; -} - -.edit-btn { - background: - linear-gradient( - 135deg, - #9B59B6, - #7D3C98 - ); - - color: white; -} - -.delete-btn { - background: - linear-gradient( - 135deg, - #E74C3C, - #922B21 - ); - - color: white; -} - -.edit-btn:hover, -.delete-btn:hover { - transform: scale(1.05); -} - -@media (max-width: 768px) { - - .add-section { - flex-direction: column; - } - - table { - font-size: 14px; - } - - th, - td { - padding: 12px; - } -} - - -/* ========================= - WHEEL PAGE -========================= */ - * { margin: 0; padding: 0; box-sizing: border-box; + font-family: 'Dana' !important; } +/* ========================= + WHEEL PAGE +========================= */ body { min-height: 100vh; - background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%); + background: radial-gradient(ellipse at center, #9B241CDD 0%, #7B241C 55%, #7B241CBB 100%); display: flex; justify-content: center; align-items: center; - font-family: Tahoma, sans-serif; + direction: rtl; +} + +.bg-rays { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 200vw; + height: 200vh; + background: repeating-conic-gradient( + rgba(255,255,255,0.03) 0deg, + rgba(255,255,255,0.03) 10deg, + transparent 10deg, + transparent 20deg + ); + pointer-events: none; + z-index: 0; } .page { + position: relative; + z-index: 1; display: flex; flex-direction: column; align-items: center; - gap: 24px; + gap: 28px; padding: 30px; } .page h1 { - color: #FFD700; - font-size: 36px; - text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); + color: #e9e3c0; + font-size: 42px; + font-weight: bold; + text-shadow: 0 0 20px rgba(255,215,0,0.5); letter-spacing: 2px; } .wheel-wrapper { position: relative; - width: 500px; - height: 500px; + width: 520px; + height: 520px; } .wheel-wrapper canvas { @@ -265,22 +72,21 @@ body { } #spin-btn { - padding: 16px 48px; - font-size: 20px; + padding: 18px 56px; + font-size: 24px; font-weight: bold; - font-family: Tahoma, sans-serif; - background: linear-gradient(135deg, #FFD700, #D4AC0D); + background: linear-gradient(135deg, #e7d78a, #f2dd9d); color: #4a0f0f; border: none; border-radius: 50px; cursor: pointer; - box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4); + box-shadow: 0 6px 25px rgba(255,215,0,0.4); transition: 0.2s; } #spin-btn:hover { transform: translateY(-3px); - box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55); + box-shadow: 0 10px 30px rgba(255,215,0,0.55); } #spin-btn:disabled { @@ -295,10 +101,10 @@ body { } .winner-text { - font-size: 28px; + font-size: 32px; font-weight: bold; color: #FFD700; - text-shadow: 0 0 20px rgba(255, 215, 0, 0.7); + text-shadow: 0 0 20px rgba(255,215,0,0.7); animation: pop 0.4s ease; } @@ -308,18 +114,380 @@ body { 100% { transform: scale(1); opacity: 1; } } +/* ========================= + مودال برنده +========================= */ +.modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal.hidden { display: none; } + +.modal-content { + background: white; + border-radius: 20px; + padding: 44px 36px; + text-align: center; + max-width: 420px; + width: 90%; + animation: popIn 0.4s ease; + direction: rtl; +} + +@keyframes popIn { + 0% { transform: scale(0.5); opacity: 0; } + 70% { transform: scale(1.05); } + 100% { transform: scale(1); opacity: 1; } +} + +.modal-icon { font-size: 70px; margin-bottom: 12px; } + +.modal-content h2 { + color: #7B241C; + font-size: 30px; + font-weight: bold; + margin-bottom: 10px; +} + +#modal-prize-type { + color: #888; + font-size: 17px; + margin-bottom: 8px; +} + +#modal-prize { + color: #C0392B; + font-size: 26px; + font-weight: bold; + margin-bottom: 14px; +} + +#modal-description { + color: #555; + font-size: 17px; + margin-bottom: 28px; + line-height: 2; +} + +#modal-close { + padding: 14px 48px; + background: linear-gradient(135deg, #FFD700, #D4AC0D); + color: #4a0f0f; + border: none; + border-radius: 50px; + font-size: 18px; + font-weight: bold; + cursor: pointer; +} + +/* ========================= + ADMIN PANEL +========================= */ +.admin-container { + width: 150%; + max-width: 1100px; + margin: 0 auto; + padding: 30px 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.admin-header { + text-align: center; + margin-bottom: 20px; +} + +.admin-header h1 { + color: #e9e8df; + font-size: 40px; + font-weight: bold; + text-shadow: 0 0 15px rgba(255,215,0,0.5); +} + +.admin-header p { + color: #f5d6a0; + margin-top: 8px; + font-size: 18px; +} + +.card { + background: rgba(0,0,0,0.25); + backdrop-filter: blur(8px); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 20px; + padding: 24px; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); +} + +.card h2 { + color: #efede1; + font-size: 20px; + font-weight: bold; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.color-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.color-section label { + color: #f5d6a0; + font-size: 16px; +} + +.color-presets { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.preset { + width: 44px; + height: 44px; + border-radius: 8px; + cursor: pointer; + border: 3px solid transparent; + transition: transform 0.2s, border-color 0.2s; +} + +.preset:hover { transform: scale(1.15); } +.preset.active { border-color: #e5e3d5; } + +.color-custom { + display: flex; + align-items: center; + gap: 12px; +} + +.color-preview { + display: flex; + align-items: center; + gap: 12px; + color: #f5d6a0; + font-size: 16px; +} + +#color-preview-box { + width: 120px; + height: 36px; + border-radius: 8px; + border: 2px solid rgba(255,255,255,0.2); + transition: background 0.3s; +} + +input[type="color"] { + width: 50px; + height: 38px; + border: none; + border-radius: 6px; + cursor: pointer; + padding: 2px; + background: transparent; +} + +#apply-color { + padding: 10px 20px; + background: rgba(255,255,255,0.15); + color: white; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + cursor: pointer; + font-size: 15px; + transition: 0.2s; +} + +#apply-color:hover { background: rgba(255,255,255,0.25); } + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group.full-width { grid-column: 1 / -1; } + +.form-group label { + font-size: 15px; + color: #f5d6a0; + font-weight: bold; +} + +.form-group input, +.form-group select, +.form-group textarea { + padding: 12px 14px; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 8px; + font-size: 15px; + background: rgba(255,255,255,0.08); + color: white; + transition: border-color 0.2s; +} + +.form-group input::placeholder, +.form-group textarea::placeholder { + color: rgba(255,255,255,0.4); +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #e4e2d7; +} + +.form-group select option { + background: #4a0f0f; + color: white; +} + +.form-group textarea { height: 90px; resize: vertical; } + +.btn-primary { + padding: 13px 32px; + background: linear-gradient(135deg, #e3dfb7, #ebe5be); + color: #4a0f0f; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: 0.2s; +} + +.btn-primary:hover { transform: translateY(-2px); } + +.btn-secondary { + padding: 13px 32px; + background: rgba(255,255,255,0.1); + color: white; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: 0.2s; +} + +.btn-secondary:hover { background: rgba(255,255,255,0.2); } + +.table-wrapper { + overflow-x: auto; + border-radius: 12px; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 15px; +} + +thead tr { + background: linear-gradient(135deg, #ebe3b9, #ebe1bc); + color: #0f374a; +} + +th, td { + padding: 14px 16px; + text-align: right; +} + +tbody tr { + border-bottom: 1px solid rgba(255,255,255,0.06); + color: white; + transition: 0.2s; +} + +tbody tr:hover { background: rgba(255,255,255,0.05); } + +.badge { + padding: 5px 12px; + border-radius: 20px; + font-size: 13px; + font-weight: bold; +} + +.badge-pooch { background: rgba(255,255,255,0.1); color: #ccc; } +.badge-discount { background: rgba(46,125,50,0.3); color: #81C784; } +.badge-coin { background: rgba(245,127,23,0.3); color: #FFD700; } + +.edit-btn { + padding: 7px 16px; + background: linear-gradient(135deg, #9B59B6, #7D3C98); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + margin-left: 6px; + transition: 0.2s; +} + +.delete-btn { + padding: 7px 16px; + background: linear-gradient(135deg, #E74C3C, #922B21); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: 0.2s; +} + +.edit-btn:hover, .delete-btn:hover { transform: scale(1.05); } + +.modal-content.admin-modal { + background: #2d0000; + color: white; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; +} + +.modal-content.admin-modal h2 { + color: #FFD700; + margin-bottom: 20px; +} + +.modal-actions { + display: flex; + gap: 12px; + margin-top: 20px; + justify-content: flex-end; +} + @media (max-width: 550px) { - .wheel-wrapper { - width: 340px; - height: 340px; - } + .wheel-wrapper { width: 340px; height: 340px; } + .page h1 { font-size: 28px; } + #spin-btn { font-size: 18px; padding: 14px 40px; } + .form-grid { grid-template-columns: 1fr; } +} - .page h1 { - font-size: 26px; - } +.title-section { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid rgba(255,255,255,0.1); +} - #spin-btn { - font-size: 16px; - padding: 12px 36px; - } +.title-section h3 { + color: #f5d6a0; + font-size: 15px; + margin-bottom: 14px; + font-family: 'Dana', Tahoma, sans-serif; } \ No newline at end of file diff --git a/frontend/assets/fonts/DanaFaNum-Regular.ttf b/frontend/assets/fonts/DanaFaNum-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..080fed21507465831376672a03f3fbbbe6108c04 GIT binary patch literal 87024 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 Y9?}PZew(Z$pk { - const row = document.createElement('tr'); - - row.innerHTML = ` - ${index + 1} - ${item.title} - ${item.value} - - - - - - - - `; - - tableBody.appendChild(row); +// ═══ رنگ پس‌زمینه ═══ +function initColor() { + const saved = getBgColor(); + colorPicker.value = saved; + previewBox.style.background = saved; + presets.forEach(p => { + if (p.dataset.color === saved) p.classList.add('active'); }); } -// -------------------- Delete -------------------- +presets.forEach(p => { + p.addEventListener('click', () => { + presets.forEach(x => x.classList.remove('active')); + p.classList.add('active'); + colorPicker.value = p.dataset.color; + previewBox.style.background = p.dataset.color; + }); +}); + +colorPicker.addEventListener('input', () => { + previewBox.style.background = colorPicker.value; + presets.forEach(x => x.classList.remove('active')); +}); + +applyColor.addEventListener('click', () => { + saveBgColor(colorPicker.value); + applyColor.textContent = '✓ ذخیره شد'; + setTimeout(() => applyColor.textContent = 'اعمال', 2000); +}); + +// ═══ عنوان صفحه گردونه ═══ +function initTitle() { + const settings = getTitleSettings(); + document.getElementById('title-text-input').value = settings.text; + document.getElementById('title-color-picker').value = settings.color; + document.getElementById('title-preview').textContent = settings.text; + document.getElementById('title-preview').style.color = settings.color; +} + +document.getElementById('title-text-input').addEventListener('input', (e) => { + document.getElementById('title-preview').textContent = e.target.value || 'گردونه شانس'; +}); + +document.getElementById('title-color-picker').addEventListener('input', (e) => { + document.getElementById('title-preview').style.color = e.target.value; +}); + +document.getElementById('apply-title').addEventListener('click', () => { + const text = document.getElementById('title-text-input').value.trim() || 'گردونه شانس'; + const color = document.getElementById('title-color-picker').value; + saveTitleSettings(text, color); + document.getElementById('apply-title').textContent = '✓ ذخیره شد'; + setTimeout(() => document.getElementById('apply-title').textContent = 'اعمال', 2000); +}); + +// ═══ badge نوع جایزه ═══ +function prizeBadge(type) { + const map = { + 'پوچ': ['badge-pooch', 'پوچ'], + 'کد تخفیف': ['badge-discount', 'کد تخفیف'], + 'لایوکوین': ['badge-coin', 'لایوکوین (LC)'], + }; + const [cls, label] = map[type] || ['badge-pooch', type]; + return `${label}`; +} + +// ═══ رندر جدول ═══ +async function renderItems() { + const items = await adminGetItems(); + tableBody.innerHTML = ''; + items.forEach((item, idx) => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${idx + 1} + ${prizeBadge(item.prize_type)} + ${item.title} + ${item.prize} + ${item.probability} + ${item.description || '—'} + + + + + `; + tableBody.appendChild(row); + }); + window._items = items; +} + +// ═══ افزودن آیتم ═══ +addBtn.onclick = async () => { + const data = { + prize_type: document.getElementById('new-prize-type').value, + title: document.getElementById('new-title').value.trim(), + prize: document.getElementById('new-prize').value.trim(), + probability: parseFloat(document.getElementById('new-probability').value), + description: document.getElementById('new-description').value.trim(), + }; + if (!data.title || !data.prize) return alert('عنوان و مقدار جایزه رو پر کن'); + await createItem(data); + document.getElementById('new-title').value = ''; + document.getElementById('new-prize').value = ''; + document.getElementById('new-description').value = ''; + document.getElementById('new-probability').value = '1'; + renderItems(); +}; + +// ═══ حذف ═══ window.removeItem = async (id) => { + if (!confirm('آیا مطمئنی؟')) return; await deleteItem(id); renderItems(); }; -// -------------------- Edit -------------------- -window.editItem = (id, title, value) => { - titleInput.value = title; - valueInput.value = value; - - editId = id; - - addBtn.textContent = 'آپدیت ✏️'; +// ═══ ویرایش ═══ +window.openEdit = (id) => { + const item = window._items.find(i => i.id === id); + if (!item) return; + document.getElementById('edit-id').value = item.id; + document.getElementById('edit-prize-type').value = item.prize_type; + document.getElementById('edit-title').value = item.title; + document.getElementById('edit-prize').value = item.prize; + document.getElementById('edit-probability').value = item.probability; + document.getElementById('edit-description').value = item.description || ''; + editModal.classList.remove('hidden'); }; -// -------------------- Add / Update -------------------- -addBtn.onclick = async () => { - const title = titleInput.value.trim(); - const value = valueInput.value.trim(); - - if (!title || !value) { - alert('همه فیلدها را پر کن'); - return; - } - - // حالت ویرایش - if (editId) { - await updateItem(editId, { - title, - value: parseInt(value) - }); - - editId = null; - addBtn.textContent = 'افزودن'; - } - - // حالت افزودن - else { - await createItem({ - title, - value: parseInt(value) - }); - } - - titleInput.value = ''; - valueInput.value = ''; +cancelEdit.onclick = () => editModal.classList.add('hidden'); +saveEdit.onclick = async () => { + const id = document.getElementById('edit-id').value; + const data = { + prize_type: document.getElementById('edit-prize-type').value, + title: document.getElementById('edit-title').value.trim(), + prize: document.getElementById('edit-prize').value.trim(), + probability: parseFloat(document.getElementById('edit-probability').value), + description: document.getElementById('edit-description').value.trim(), + }; + await updateItem(id, data); + editModal.classList.add('hidden'); renderItems(); }; -// -------------------- Init -------------------- +initColor(); +initTitle(); renderItems(); \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js index 17958bc..327f1d6 100644 --- a/frontend/assets/js/api.js +++ b/frontend/assets/js/api.js @@ -1,40 +1,74 @@ const BASE = 'http://127.0.0.1:8000'; +// ---------------- USER ---------------- export const getItems = () => - fetch(`${BASE}/user/items`) - .then(r => r.json()); + fetch(`${BASE}/user/items`).then(r => r.json()); export const spinWheel = () => - fetch(`${BASE}/user/spin`, { - method: 'POST' - }).then(r => r.json()); + fetch(`${BASE}/user/spin`, { method: 'POST' }).then(async (r) => { + if (!r.ok) throw new Error('Spin failed'); + return r.json(); + }); +// ---------------- ADMIN ---------------- export const adminGetItems = () => - fetch(`${BASE}/admin/items`) - .then(r => r.json()); + fetch(`${BASE}/admin/items`).then(r => r.json()); export const createItem = (data) => fetch(`${BASE}/admin/items`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: data.title, + prize: data.prize, + prize_type: data.prize_type, + description: data.description, + probability: data.probability + }) }).then(r => r.json()); export const updateItem = (id, data) => fetch(`${BASE}/admin/items/${id}`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: data.title, + prize: data.prize, + prize_type: data.prize_type, + description: data.description, + probability: data.probability + }) }).then(r => r.json()); export const deleteItem = (id) => - fetch(`${BASE}/admin/items/${id}`, { - method: 'DELETE' - }).then(r => { - if (r.status === 204) return {}; - return r.json(); - }); \ No newline at end of file + fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(async (r) => { + if (!r.ok && r.status !== 204) throw new Error('Delete failed'); + return {}; + }); + +// ---------------- رنگ پس‌زمینه ---------------- +export const getBgColor = () => + localStorage.getItem('wheelBgColor') || '#7B241C'; + +export const saveBgColor = (color) => { + localStorage.setItem('wheelBgColor', color); + window.dispatchEvent(new StorageEvent('storage', { + key: 'wheelBgColor', + newValue: color + })); +}; + +// ---------------- عنوان صفحه گردونه ---------------- +export const getTitleSettings = () => { + const saved = localStorage.getItem('wheelTitleSettings'); + return saved ? JSON.parse(saved) : { text: 'گردونه شانس', color: '#FFD700' }; +}; + +export const saveTitleSettings = (text, color) => { + const settings = { text, color }; + localStorage.setItem('wheelTitleSettings', JSON.stringify(settings)); + window.dispatchEvent(new StorageEvent('storage', { + key: 'wheelTitleSettings', + newValue: JSON.stringify(settings) + })); +}; \ No newline at end of file diff --git a/frontend/assets/js/wheel.js b/frontend/assets/js/wheel.js index e4ca45c..5185b5f 100644 --- a/frontend/assets/js/wheel.js +++ b/frontend/assets/js/wheel.js @@ -1,4 +1,4 @@ -import { getItems, spinWheel } from './api.js'; +import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js'; const canvas = document.getElementById('wheel'); const ctx = canvas.getContext('2d'); @@ -8,12 +8,43 @@ const result = document.getElementById('result'); const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' }; const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' }; +let audioCtx = null; +function getAudioCtx() { + if (!audioCtx || audioCtx.state === 'closed') { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + if (audioCtx.state === 'suspended') audioCtx.resume(); + return audioCtx; +} + let items = []; let angle = 0; let spinning = false; let lampOn = true; +function applyBg() { + const color = getBgColor(); + document.getElementById('main-body').style.background = + `radial-gradient(ellipse at center, ${color}DD 0%, ${color} 55%, ${color}BB 100%)`; +} + +function applyTitle() { + const settings = getTitleSettings(); + const h1 = document.querySelector('.page h1'); + if (h1) { + h1.textContent = settings.text; + h1.style.color = settings.color; + h1.style.textShadow = `0 0 20px ${settings.color}80`; + } +} + async function init() { + applyBg(); + applyTitle(); + window.addEventListener('storage', (e) => { + if (e.key === 'wheelBgColor') applyBg(); + if (e.key === 'wheelTitleSettings') applyTitle(); + }); items = await getItems(); draw(angle); setInterval(() => { @@ -75,7 +106,6 @@ function draw(rotation) { ctx.clearRect(0, 0, canvas.width, canvas.height); - // ═══ سایه کلی ═══ ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.shadowBlur = 30; @@ -87,7 +117,6 @@ function draw(rotation) { ctx.fill(); ctx.restore(); - // ═══ حلقه بیرونی تیره ═══ const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40); rimGrad.addColorStop(0, '#A0003A'); rimGrad.addColorStop(0.3, '#6D0025'); @@ -98,14 +127,12 @@ function draw(rotation) { ctx.fillStyle = rimGrad; ctx.fill(); - // ═══ بخش‌های گردونه ═══ items.forEach((item, i) => { const start = rotation + i * slice; const end = start + slice; drawSegment(cx, cy, r, start, end, getTheme(i, total)); }); - // ═══ خطوط طلایی ═══ items.forEach((_, i) => { const lineAngle = rotation + i * slice; const lGrad = ctx.createLinearGradient( @@ -113,7 +140,7 @@ function draw(rotation) { cx + r * Math.cos(lineAngle), cy + r * Math.sin(lineAngle) ); - lGrad.addColorStop(0, 'rgba(700,550,0,5)'); + lGrad.addColorStop(0, 'rgba(255,215,0,0.3)'); lGrad.addColorStop(0.4, '#a2922e'); lGrad.addColorStop(1, '#c8b43b'); ctx.beginPath(); @@ -124,31 +151,29 @@ function draw(rotation) { ctx.stroke(); }); - // ═══ متن آیتم‌ها ═══ items.forEach((item, i) => { const start = rotation + i * slice; const theme = getTheme(i, total); ctx.save(); ctx.translate(cx, cy); ctx.rotate(start + slice / 2); - ctx.textAlign = 'right'; + ctx.textAlign = 'center'; ctx.fillStyle = theme.text; - ctx.font = 'bold 12px Tahoma'; + ctx.font = 'bold 18px Dana'; ctx.shadowColor = 'rgba(0,0,0,0.6)'; ctx.shadowBlur = 4; const text = item.title; const maxWidth = r - 52; if (ctx.measureText(text).width > maxWidth) { const mid = Math.ceil(text.length / 2); - ctx.fillText(text.slice(0, mid), r - 20, -7); - ctx.fillText(text.slice(mid), r - 20, 9); + ctx.fillText(text.slice(0, mid), (r - 20) / 1.4, -9); + ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9); } else { - ctx.fillText(text, r - 20, 5); + ctx.fillText(text, (r - 20) / 1.4, 5); } ctx.restore(); }); - // ═══ حلقه طلایی داخلی ═══ ctx.beginPath(); ctx.arc(cx, cy, r + 2, 0, Math.PI * 2); ctx.strokeStyle = '#dfd498'; @@ -158,7 +183,6 @@ function draw(rotation) { ctx.stroke(); ctx.shadowBlur = 0; - // ═══ لامپ‌ها ═══ const lampCount = 24; const lampR = r + 23; for (let i = 0; i < lampCount; i++) { @@ -208,7 +232,6 @@ function draw(rotation) { ctx.fill(); } - // ═══ حلقه طلایی بیرونی ═══ ctx.beginPath(); ctx.arc(cx, cy, r + 35, 0, Math.PI * 2); ctx.strokeStyle = '#C8960C'; @@ -218,7 +241,6 @@ function draw(rotation) { ctx.stroke(); ctx.shadowBlur = 0; - // ═══ دایره مرکزی — هماهنگ با نشانگر ═══ const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22); cGrad.addColorStop(0, '#FFFFF0'); cGrad.addColorStop(0.15, '#FFE566'); @@ -240,18 +262,15 @@ function draw(rotation) { ctx.lineWidth = 2; ctx.stroke(); - // بازتاب روی دایره مرکزی ctx.beginPath(); ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fill(); - // ═══ نشانگر ═══ drawPointer(cx); } function drawPointer(cx) { - // سایه ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.4)'; ctx.shadowBlur = 8; @@ -263,7 +282,6 @@ function drawPointer(cx) { ctx.fill(); ctx.restore(); - // دایره طلایی — همرنگ مرکز const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18); pGrad.addColorStop(0, '#FFFFF0'); pGrad.addColorStop(0.15, '#FFE566'); @@ -282,13 +300,11 @@ function drawPointer(cx) { ctx.lineWidth = 2; ctx.stroke(); - // بازتاب ctx.beginPath(); ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fill(); - // مثلث — همرنگ const triGrad = ctx.createLinearGradient(cx, 34, cx, 62); triGrad.addColorStop(0, '#FFE566'); triGrad.addColorStop(0.35, '#FFD700'); @@ -309,6 +325,52 @@ function drawPointer(cx) { ctx.stroke(); } +function playTickSound() { + try { + const actx = getAudioCtx(); + const osc = actx.createOscillator(); + const gain = actx.createGain(); + osc.connect(gain); + gain.connect(actx.destination); + osc.frequency.value = 600; + osc.type = 'sine'; + gain.gain.setValueAtTime(0.15, actx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.08); + osc.start(actx.currentTime); + osc.stop(actx.currentTime + 0.08); + } catch(e) {} +} + +function playWinSound() { + try { + const actx = getAudioCtx(); + const notes = [523, 659, 784, 1047]; + notes.forEach((freq, i) => { + const osc = actx.createOscillator(); + const gain = actx.createGain(); + osc.connect(gain); + gain.connect(actx.destination); + osc.frequency.value = freq; + osc.type = 'sine'; + gain.gain.setValueAtTime(0.2, actx.currentTime + i * 0.15); + gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + i * 0.15 + 0.3); + osc.start(actx.currentTime + i * 0.15); + osc.stop(actx.currentTime + i * 0.15 + 0.3); + }); + } catch(e) {} +} + +function showWinnerModal(winner) { + const modal = document.getElementById('winner-modal'); + const iconMap = { 'پوچ': '', 'کد تخفیف': '', 'لایوکوین': '' }; + document.getElementById('modal-icon').textContent = iconMap[winner.prize_type] || '🎉'; + document.getElementById('modal-prize-type').textContent = winner.prize_type; + document.getElementById('modal-prize').textContent = winner.prize; + document.getElementById('modal-description').textContent = winner.description || ''; + modal.classList.remove('hidden'); + document.getElementById('modal-close').onclick = () => modal.classList.add('hidden'); +} + btn.onclick = async () => { if (spinning || items.length === 0) return; spinning = true; @@ -328,12 +390,20 @@ btn.onclick = async () => { const duration = 5500; const startTime = performance.now(); const from = angle; + let lastTickIdx = 0; function animate(now) { const elapsed = now - startTime; const t = Math.min(elapsed / duration, 1); const ease = 1 - Math.pow(1 - t, 4); angle = from + (targetAngle - from) * ease; + + const currentIdx = Math.floor(Math.abs(angle) / slice); + if (currentIdx !== lastTickIdx) { + lastTickIdx = currentIdx; + playTickSound(); + } + draw(angle); if (t < 1) { @@ -342,7 +412,8 @@ btn.onclick = async () => { spinning = false; btn.disabled = false; draw(angle); - result.innerHTML = `🎉 ${winner.title} 🎉`; + playWinSound(); + showWinnerModal(winner); } } diff --git a/frontend/index.html b/frontend/index.html index a2039d2..7bd231d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,18 +2,34 @@ + گردونه شانس + - + +

-

گردونه شانس

+

گردونه شانس

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