From 2d99f0554db35a54039b7cebf1de56cdc2de1305 Mon Sep 17 00:00:00 2001 From: Anahita-Mahmoudi Date: Sun, 24 May 2026 02:27:31 +0330 Subject: [PATCH] feat: add user and admin page --- backend/__pycache__/main.cpython-313.pyc | Bin 638 -> 669 bytes .../__pycache__/a12a2ecd70b9_.cpython-313.pyc | Bin 0 -> 978 bytes .../ab09ab5070f7_initial.cpython-313.pyc | Bin 0 -> 979 bytes backend/alembic/versions/a12a2ecd70b9_.py | 28 ++++++ .../alembic/versions/ab09ab5070f7_initial.py | 28 ++++++ backend/api/__pycache__/admin.cpython-313.pyc | Bin 1692 -> 1944 bytes backend/api/admin.py | 39 +++------ .../db/__pycache__/session.cpython-313.pyc | Bin 554 -> 554 bytes backend/main.py | 5 +- backend/wheel.db | Bin 12288 -> 20480 bytes frontend-admin/admin.html | 0 frontend-admin/app.js | 0 frontend-admin/index.html | 0 frontend/admin.html | 20 +++++ frontend/assets/css/style.css | 61 +++++++++++++ frontend/assets/js/admin.js | 33 +++++++ frontend/assets/js/api.js | 20 +++++ frontend/assets/js/wheel.js | 82 ++++++++++++++++++ frontend/index.html | 17 ++++ 19 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 backend/alembic/versions/__pycache__/a12a2ecd70b9_.cpython-313.pyc create mode 100644 backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc create mode 100644 backend/alembic/versions/a12a2ecd70b9_.py create mode 100644 backend/alembic/versions/ab09ab5070f7_initial.py delete mode 100644 frontend-admin/admin.html delete mode 100644 frontend-admin/app.js delete mode 100644 frontend-admin/index.html create mode 100644 frontend/admin.html create mode 100644 frontend/assets/css/style.css create mode 100644 frontend/assets/js/admin.js create mode 100644 frontend/assets/js/api.js create mode 100644 frontend/assets/js/wheel.js create mode 100644 frontend/index.html diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 02039a97118eefe5c052b8a4101b9bd69dea7090..5524bfe5ccc32d28770e92cd60b657bbe90099eb 100644 GIT binary patch delta 152 zcmeyzGMAP2GcPX}0}!kh70Nm?kyny&-9+_vonZE04ofBlh8WHuagZ!3h~Wt4gvh6J zX>w1zS0q`*rk|LSo0+G{c#Ex|C^ap!qKZ|&v^ceBascBlE(xG^Mj$TEnJmH-qOgMF e0)x;+2E!W+%-2B-p$XOt%7H?c81#$8fXV>f!y}IX delta 120 zcmbQs`j3V8GcPX}0}xm+a%L@^$ScXXX`*_&fhi+MjDbOcA%-(Z9KuE>V>l2h(%CgR uCf+KVoXWV13#1gJwK!?ACR2#m6$XU|49wRVgf21&O|ZVoAYUX3lmq~<x1s%}(ECU)mWM zaU>Rtj+)}JT#@*b;Yxzbki7&~hq!(Tt|ho`k4&I-8FIF6XA>{qlnc47HPY^M`$n6@ zF>Vq~drCH2aVrXqCkr(Ln-;c+=U2;(xl@G1HDhkZUtOK@E8eQG&8PxH z9yEKg9o7BlW$2!RdINCit+@eikRYa-PdX&@V>b#t-tO!O%#qkV;oI6pO`#xeRODXOG&AToR z!>Eh9d~DYRm6zbZ1&naRr%ibLO;dT;j{IIg9>GnP!WR+unK_mOLHI7F1^HJF36o#Z z^-t)=v4X~K50vf6Lv-uNoFC}hy&e1Qy}hY-#rN0t$BPH~(jl5ZGN=EIOdpl3fwsN4 gGr99>PkQru&)>gt{~-I|5LrLcqM-iNm`UXM4FRa~jsO4v literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc b/backend/alembic/versions/__pycache__/ab09ab5070f7_initial.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94d43be7c90f994321458a3dbef4065d01f2bf56 GIT binary patch literal 979 zcmah|&ubGw6rTN&WV6jCty+a5x==)8N!YB3Awk-M?ZFm$a4B9Emf77gP2KLMZ#T6m z2s!px4+XvTs5k$VmR`zu5yXSHVim!YGutRtEDp@u_vU;1-uJ#?PBl$MZ2j2Eug!=E zedNKfC6Aobcg)#Cw~&PZS%M`l35z0#ixNmWkEJDfF$0;@mzT1TvofIAd0Vxzx7nXo z4#sSW#iE_h@K~-({L649#bqd5fagcJb^)%YxMqz_p*0zbmSz=Fuhfyt#cj76c9Sj+ zRP{00?8e^6+1@w9n{keHp;Ol%U*ZyIh(eFy|6M4xA-t(%5Ahdmf38n>Ss0wDvMOH$VQ)p9-*0)kTog`RE&Ddv~a!|Rba$}ou6w* zH9vYDI=?~91sr;-PJmq!#8mZ3kA!~gM4`vq{aONZB(_iaJ?){6P!_jKE4>b2pXjl- zO4f08`7HvC=dg|F>_XeNdfiZevPwu`yV!fi3fnj!>u%SxHwkc&V*5O4XCz{^H=w!x zS#=m^)h2maevd=+K~|v1y#Lahm}i6jNid~%W_SrGF+=lQkTef2vv`UF6Ws6=CU+J{ z(uZ)lJWg|t<42z3&}=ebqji8w0$dWB8x01WB~{~%00&%_0Tp8mIesX`3DA<`;4q95 zobc%#2lBiGe-|*u4WBmU@i$H7!FuHP12PNOSqi_8c)-kwBnZM6F)PU5i%6LMgs#3v z*G?2PadW8bOdp{e$F;ekw$tCWUftQh_qy`t%E4siuv9%lbH}yDzmdjq)f}oj3%k?1 dTl><>7yJIfrMriPnImL=&5A<)o61Zo&relu^5p;k literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/a12a2ecd70b9_.py b/backend/alembic/versions/a12a2ecd70b9_.py new file mode 100644 index 0000000..5ec8cda --- /dev/null +++ b/backend/alembic/versions/a12a2ecd70b9_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: a12a2ecd70b9 +Revises: 96dff3d4cf1c +Create Date: 2026-05-17 09:46:42.181807 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a12a2ecd70b9' +down_revision: Union[str, Sequence[str], None] = '96dff3d4cf1c' +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/ab09ab5070f7_initial.py b/backend/alembic/versions/ab09ab5070f7_initial.py new file mode 100644 index 0000000..e6efba1 --- /dev/null +++ b/backend/alembic/versions/ab09ab5070f7_initial.py @@ -0,0 +1,28 @@ +"""initial + +Revision ID: ab09ab5070f7 +Revises: a12a2ecd70b9 +Create Date: 2026-05-17 11:44:50.049279 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ab09ab5070f7' +down_revision: Union[str, Sequence[str], None] = 'a12a2ecd70b9' +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 7fd52b44df1094beeb0f1f509690a5bdfdc1e823..e10c474fb61d1024e219d90977187b5af543b87a 100644 GIT binary patch delta 1180 zcmZuv&rcIU6rR~1?RK}jEmTmt6ifN71T~h3iE;om3I`L`1BXov!iJO-+RUupwB@8h z6D4~g(F4YVlK3z9KP-|cZmcm;58k%IAK;rQHH0w9eDmhbyzhJS-n`Z~6UM%-s|dz= zd+TzCv|*gV%Nqj|mB@6IM6<|3R&++0j*%F*l^JzfBU%>OIEO}A7z`4(VrfKlJ3gCf zc#fi_+6g-$)q0GqeMufld9(Ps9>486E3*zQmsY9c zJ;xv7F%Svlg<^?1+uoEg)|UiF3rso)@CQ}Vw^-Y=uIfZ6h;nMrNUkeC6xDkxOm-3j zZY&%Y3QUB=!?{1=DPsRTY9`x@hz3VP0#RTIVoQ#DuK<})qQjmtiY!S0l2jAY?11G_ zMZdJfi}CgG&D^e*@ukc@I8h+6c&{$B;|b8}6XufMCGo7oWyFSzwm_VD*WpTm>=xXj z0~xeIG|!w0U}X<=PcturEKJ$~P(=qSiYLEl<|obEoOq|cQFpcTzI2|P@;-`rJlB*p zrAHP1EN#&#I){3MeWxLjFea|Xbtx-QwyD|0&KR&p(~AJdGeP2F@y#&bi++A; z$GG7uH}>LfFCY6_zc2NZQ!M&);GL64Dv^g4_t>3*6KinW&~2PKwVz^Kq{B08>sA~O!E5ivILRF;{vg;>hh$w5m)DJ3zTas zmD1vaqBBm0fs{3reg+6djByQRYRIgi?%yc)6B#v>t)Y(JXrLZJYU-I;jr@(sI2E=c zL3xs1OFzA}b@%=7PT!Sk+Q(yGJA1ZnzPeLK7*42vyrM8Qtv`v^zy-DLq zvSfDL(rT;$ARwyDc`2DWPrqyNj5spnjyuFT4A_Mcq# zGo?BbyeD^q6LM%U%oHhWn@!)V1_#ouiiM6WZvPs>cj&;WHnum>S%_s>u|q5ShQ%sq z>ENeSD@Ssp?QL-bnQIclaLH&;-2KJi0iCAgAq3;{RVJocr92hflMBm2BZ>4yWU0hW z;m8f4h=xCj(1G(D7}?{r^Cj(sOb?myR)4Mc6>6w4P(|%S@KK&6k1nKs%P)3Q7#XG4 zy-ly}JxPjG(1YS*cqFnsX;M^K8j2Uhoq+4O{Tc#_8-I&u65-C`FCG1-mJ{Zk(4Oik zG^1lRnjRaV<=*AyV=9UIBt_+;n5a4$oGUkXBj$Vjd1KAvLw)2}FCOW|nBv!0mN^&RzJ|n@ e2{XgBJA+=ECNKXF1{S_>27X@toqV(T!Z#K^=B;;P zVG~ytXN=6uOG&NBtccGnNzE;e&rAW+tj#AW6h1_H@j|Zy4kK^%+G`>V8qMF%%BKTak~ek pf(vZoD+c~Q{NMOL@V^3@c#B_*m6?%|g#|=1GlNJbCT3<%P5?j!YfAtC delta 56 zcmZozz}S#5L0XWJfq{V;h+%+nqK+|8P_Ipsm;VO?6F)BlKQI5z&4L1R`8M + + + + پنل ادمین + + + +

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

+ +
+ + +
+ +
    + + + + \ No newline at end of file diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css new file mode 100644 index 0000000..b183966 --- /dev/null +++ b/frontend/assets/css/style.css @@ -0,0 +1,61 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: Tahoma, sans-serif; + background: #f5f5f5; + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 20px; + gap: 20px; +} + +h1 { + color: #2c3e50; + margin-bottom: 10px; +} + +canvas { + border-radius: 50%; + box-shadow: 0 4px 20px rgba(0,0,0,0.2); +} + +button { + padding: 12px 30px; + font-size: 18px; + font-family: Tahoma, sans-serif; + background: #e74c3c; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} + +button:hover { + background: #c0392b; +} + +button:disabled { + background: #aaa; + cursor: not-allowed; +} + +#result { + font-size: 24px; + font-weight: bold; + color: #2c3e50; +} + +/* ادمین */ +#add-section { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +input { + padding: 10px; \ No newline at end of file diff --git a/frontend/assets/js/admin.js b/frontend/assets/js/admin.js new file mode 100644 index 0000000..2058645 --- /dev/null +++ b/frontend/assets/js/admin.js @@ -0,0 +1,33 @@ +import { adminGetItems, createItem, deleteItem } from './api.js'; + +const list = document.getElementById('items-list'); +const input = document.getElementById('new-item'); +const addBtn = document.getElementById('add-btn'); + +async function renderItems() { + const items = await adminGetItems(); + list.innerHTML = ''; + items.forEach(item => { + const li = document.createElement('li'); + li.innerHTML = ` + ${item.title} + + `; + list.appendChild(li); + }); +} + +window.removeItem = async (id) => { + await deleteItem(id); + renderItems(); +}; + +addBtn.onclick = async () => { + const title = input.value.trim(); + if (!title) return; + await createItem(title); + input.value = ''; + renderItems(); +}; + +renderItems(); \ No newline at end of file diff --git a/frontend/assets/js/api.js b/frontend/assets/js/api.js new file mode 100644 index 0000000..a55795f --- /dev/null +++ b/frontend/assets/js/api.js @@ -0,0 +1,20 @@ +const BASE = 'http://127.0.0.1:8000'; + +export const getItems = () => + fetch(`${BASE}/user/items`).then(r => r.json()); + +export const spinWheel = () => + fetch(`${BASE}/user/spin`, { method: 'POST' }).then(r => r.json()); + +export const adminGetItems = () => + fetch(`${BASE}/admin/items`).then(r => r.json()); + +export const createItem = (title) => + fetch(`${BASE}/admin/items`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }) + }).then(r => r.json()); + +export const deleteItem = (id) => + fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(r => r.json()); \ No newline at end of file diff --git a/frontend/assets/js/wheel.js b/frontend/assets/js/wheel.js new file mode 100644 index 0000000..83cc99b --- /dev/null +++ b/frontend/assets/js/wheel.js @@ -0,0 +1,82 @@ +import { getItems, spinWheel } 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']; + +let items = []; +let angle = 0; +let spinning = false; + +async function init() { + items = await getItems(); + drawWheel(angle); +} + +function drawWheel(rotation) { + if (items.length === 0) return; + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const r = cx - 10; + const slice = (2 * Math.PI) / items.length; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + items.forEach((item, i) => { + const start = rotation + i * slice; + const end = start + slice; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r, start, end); + ctx.fillStyle = colors[i % colors.length]; + ctx.fill(); + ctx.stroke(); + 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.restore(); + }); +} + +btn.onclick = async () => { + if (spinning || items.length === 0) return; + spinning = true; + btn.disabled = true; + result.textContent = ''; + + const data = await spinWheel(); + 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 from = angle; + + function animate(now) { + const elapsed = now - start; + const t = Math.min(elapsed / duration, 1); + const ease = 1 - Math.pow(1 - t, 3); + angle = from + (targetAngle - from) * ease; + drawWheel(angle); + + if (t < 1) { + requestAnimationFrame(animate); + } else { + spinning = false; + btn.disabled = false; + result.textContent = `🎉 ${winner.title}`; + } + } + + requestAnimationFrame(animate); +}; + +init(); \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0713603 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + گردونه شانس + + + +

    گردونه شانس

    + +
    + +

    + + + + \ No newline at end of file