From fbb3e7d8503844348e476925d40fac8cf4404a86 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 1 May 2026 19:14:39 +0300 Subject: [PATCH] Add brands management system with backend persistence --- .gitignore | 1 + backend/.env.example | 2 +- .../app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 173 bytes .../app/__pycache__/config.cpython-314.pyc | Bin 0 -> 1389 bytes backend/app/__pycache__/main.cpython-314.pyc | Bin 0 -> 3203 bytes backend/app/__pycache__/utils.cpython-314.pyc | Bin 0 -> 2071 bytes backend/app/config.py | 3 +- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 182 bytes .../__pycache__/database.cpython-314.pyc | Bin 0 -> 835 bytes backend/app/main.py | 27 +- backend/app/models/__init__.py | 2 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 616 bytes .../models/__pycache__/brand.cpython-314.pyc | Bin 0 -> 731 bytes .../models/__pycache__/cart.cpython-314.pyc | Bin 0 -> 1761 bytes .../__pycache__/category.cpython-314.pyc | Bin 0 -> 849 bytes .../contact_message.cpython-314.pyc | Bin 0 -> 973 bytes .../models/__pycache__/model.cpython-314.pyc | Bin 0 -> 1442 bytes .../models/__pycache__/order.cpython-314.pyc | Bin 0 -> 2254 bytes .../__pycache__/product.cpython-314.pyc | Bin 0 -> 2048 bytes .../models/__pycache__/user.cpython-314.pyc | Bin 0 -> 1852 bytes .../__pycache__/wishlist.cpython-314.pyc | Bin 0 -> 1125 bytes backend/app/models/brand.py | 8 + backend/app/models/category.py | 1 + backend/app/models/model.py | 21 + backend/app/models/product.py | 7 +- backend/app/models/user.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 181 bytes .../routers/__pycache__/auth.cpython-314.pyc | Bin 0 -> 3639 bytes .../__pycache__/brands.cpython-314.pyc | Bin 0 -> 5748 bytes .../routers/__pycache__/cart.cpython-314.pyc | Bin 0 -> 4040 bytes .../__pycache__/categories.cpython-314.pyc | Bin 0 -> 4727 bytes .../__pycache__/contact.cpython-314.pyc | Bin 0 -> 1296 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 6660 bytes .../__pycache__/orders.cpython-314.pyc | Bin 0 -> 2808 bytes .../__pycache__/products.cpython-314.pyc | Bin 0 -> 11345 bytes .../routers/__pycache__/users.cpython-314.pyc | Bin 0 -> 2587 bytes .../__pycache__/wishlist.cpython-314.pyc | Bin 0 -> 3941 bytes backend/app/routers/auth.py | 2 +- backend/app/routers/brands.py | 89 ++ backend/app/routers/cart.py | 51 +- backend/app/routers/categories.py | 23 +- backend/app/routers/models.py | 105 ++ backend/app/routers/orders.py | 41 +- backend/app/routers/products.py | 143 +- backend/app/routers/users.py | 35 +- backend/app/routers/wishlist.py | 44 +- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 181 bytes .../schemas/__pycache__/brand.cpython-314.pyc | Bin 0 -> 1660 bytes .../schemas/__pycache__/cart.cpython-314.pyc | Bin 0 -> 2894 bytes .../__pycache__/category.cpython-314.pyc | Bin 0 -> 2335 bytes .../__pycache__/contact.cpython-314.pyc | Bin 0 -> 1682 bytes .../schemas/__pycache__/model.cpython-314.pyc | Bin 0 -> 2849 bytes .../schemas/__pycache__/order.cpython-314.pyc | Bin 0 -> 3333 bytes .../__pycache__/product.cpython-314.pyc | Bin 0 -> 4835 bytes .../schemas/__pycache__/user.cpython-314.pyc | Bin 0 -> 2934 bytes backend/app/schemas/brand.py | 16 + backend/app/schemas/category.py | 3 + backend/app/schemas/model.py | 36 + backend/app/schemas/product.py | 17 +- backend/app/schemas/user.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 182 bytes .../services/__pycache__/auth.cpython-314.pyc | Bin 0 -> 5836 bytes .../services/__pycache__/cart.cpython-314.pyc | Bin 0 -> 5281 bytes .../__pycache__/order.cpython-314.pyc | Bin 0 -> 4962 bytes .../__pycache__/product.cpython-314.pyc | Bin 0 -> 5134 bytes backend/app/services/auth.py | 45 +- backend/app/utils.py | 32 + backend/migrations/001_add_model_table.sql | 24 + backend/migrations/002_add_brands_table.sql | 24 + backend/requirements.txt | 22 +- backend/schema.sql | 132 +- ecommerce.db | Bin 0 -> 122880 bytes frontend/src/App.jsx | 4 + frontend/src/api.js | 5 +- frontend/src/components/CategoryCard.jsx | 2 +- frontend/src/components/Navbar.jsx | 3 + frontend/src/components/ProductFilters.jsx | 140 +- frontend/src/context/AuthContext.jsx | 21 +- frontend/src/context/CartContext.jsx | 7 +- frontend/src/pages/Admin.jsx | 1224 +++++++++++++++++ frontend/src/pages/Cart.jsx | 1 - frontend/src/pages/Checkout.jsx | 4 +- frontend/src/pages/Login.jsx | 8 +- frontend/src/pages/Models.jsx | 331 +++++ frontend/src/pages/Orders.jsx | 4 +- frontend/src/pages/ProductDetail.jsx | 79 +- frontend/src/pages/Products.jsx | 15 +- frontend/src/pages/Profile.jsx | 20 +- frontend/src/pages/Wishlist.jsx | 8 +- frontend/src/styles/global.css | 77 ++ 90 files changed, 2686 insertions(+), 225 deletions(-) create mode 100644 backend/app/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/__pycache__/config.cpython-314.pyc create mode 100644 backend/app/__pycache__/main.cpython-314.pyc create mode 100644 backend/app/__pycache__/utils.cpython-314.pyc create mode 100644 backend/app/database/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/database/__pycache__/database.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/brand.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/cart.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/category.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/contact_message.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/model.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/order.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/product.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/user.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/wishlist.cpython-314.pyc create mode 100644 backend/app/models/brand.py create mode 100644 backend/app/models/model.py create mode 100644 backend/app/routers/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/auth.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/brands.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/cart.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/categories.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/contact.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/models.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/orders.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/products.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/users.cpython-314.pyc create mode 100644 backend/app/routers/__pycache__/wishlist.cpython-314.pyc create mode 100644 backend/app/routers/brands.py create mode 100644 backend/app/routers/models.py create mode 100644 backend/app/schemas/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/brand.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/cart.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/category.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/contact.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/model.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/order.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/product.cpython-314.pyc create mode 100644 backend/app/schemas/__pycache__/user.cpython-314.pyc create mode 100644 backend/app/schemas/brand.py create mode 100644 backend/app/schemas/model.py create mode 100644 backend/app/services/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/services/__pycache__/auth.cpython-314.pyc create mode 100644 backend/app/services/__pycache__/cart.cpython-314.pyc create mode 100644 backend/app/services/__pycache__/order.cpython-314.pyc create mode 100644 backend/app/services/__pycache__/product.cpython-314.pyc create mode 100644 backend/app/utils.py create mode 100644 backend/migrations/001_add_model_table.sql create mode 100644 backend/migrations/002_add_brands_table.sql create mode 100644 ecommerce.db create mode 100644 frontend/src/pages/Admin.jsx create mode 100644 frontend/src/pages/Models.jsx diff --git a/.gitignore b/.gitignore index bf38fa5..a9f2576 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ backend/*.pyc backend/.pytest_cache/ backend/.vscode/ backend/instance/ +uploads/ # Frontend frontend/node_modules/ diff --git a/backend/.env.example b/backend/.env.example index a742e65..44eeb90 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,4 @@ -DATABASE_URL=postgresql://user:password@localhost:5432/ecommerce_db +DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db JWT_SECRET_KEY=your-secret-key-here-change-this-in-production JWT_ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=30 diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c6f4bf5768e31a511bc65a1c55f5e06797fbca2 GIT binary patch literal 173 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%gfm+CbT%U zs5mC2EVC#l#y>CBr6{v3HO3{iIJ+djASOMtBsDQ6sVFfoMK?FGxFoeGCMhvFJ2fvQ wCb6I(CO$qhFS8^*Uaz3?7Kcr4eoARhs$CH)&}fk5#UREfW=2NFB4!{90Kdm9s{jB1 literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/config.cpython-314.pyc b/backend/app/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2593fb5afa59df9b41940417d05b53a8bebc4071 GIT binary patch literal 1389 zcmZux-)|d55T3m|UoMH0s3eXnT1ZmF4?{=a z5=e!RRZu~K2M{m(3%v2CU?J3Yr9L2@c&kaI^4OiTlNyPYZgysNXZD-fZ|}A7u@SIC z{csZBWdMJXaXJcp<@kzK+OPv5yAJCN*$nBfF3fX+?<-D$`y z6!dnUw}~C}p8{BRdT}_k1xqX}lpzT)2z?yh0vrj4EQr7arze}j`sSOL-@09DAhuQ( z7n3X!Nh8y^a`}z7m;Qt2tQ8G=zyO*ex>V?P!G8X-Z6J-C+VDv$P)X-S!q_diV=$R7Om|c`BNcb2GwuGpijWq!&5=^gty2l?l%4 zvC^2tsZ2Aw>}phsx=Z2pI1lk#Ldg_OU{mKlQ{r5sz&PRpQN_ClC^hGbsB%n*&HB}q z>PK2CU9BC)Dye>$%4;e9L{CWMSFYlG-JLUO%<)uz}>DT%n<<;LO-}^pLQ+Ah{ zwj!dY_ZtsX7%}j8%ZgRjhAr4}-uW7!X1y2T7HhCY*km8WyH3bnvU~cvGoZVDo!LF! zfWJU`&SD~&Db@svUCU#Bpb|6w$!Ey5jFd#aFOSR^*&^A?RE&+LxgerQYRxgT9pLiw zR;(m%#wqpH6zeKW(RPM1RU9=z>D@TRievnGrkVoH80S$Uw64Vw+PfhHm)fe71L0hx zX=bmBuD^6fm_awuSJNy0_Zzqa51uJ?yi(T%#{Qftcf4}9VAZYc=M|=A?TV%=eLt^I`Bu$EUdAidvSqa4kq^(#wLiH3a^*K~=KlG`_Kn9LE39?F zipLWc>Q~SMOYs{O-<)e5)kI3H*8P8vBC?pFmi_%C@oxSu@`Frpwu36|rOCS;toB~o pnj#zoQqNlP$YqQ@gjXNJ`4c~2Yu|ox0(R<7LviZ6l{-DYJ^?PTPdNYp literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d50a8604aa010187733550687b4038fbb61d026 GIT binary patch literal 3203 zcmbVO&vVbH! zXR(xP^+L4=dBzv%BA329hWrm_~VV zS!jFa`nu55_i{(g`SYCGxptn7%J~qCDR;-rquk)DDBgM-X0&DynndUK=hR-zJ@o3x z%Yn8}l4m4l`?Uy-__~yhYB3t~^-EWHdu{o+9zBDLOd~y}EBb&wm=W9FbS^)a`}iDWEI*w4q}Q$w>G8A( z@u+FRe=cQ!8T`+@yl~iqh0psV+qtfUFgq%f9H9Rzx19s zysf+dJ_1a%1G`0B%FaW{xd)Q12=s#FEA-*&?3A; zYlcT}<+7kEx9{qoer#KogYO#zgC+7QHK=W_*$(#R15C!{ExU}F^uFQo?y!d`waZ)H zd_R+nDlM{L)x*SN;R}7-D_)A$646K z2CWiorSL1gsZE!pOvj}~h&KhJr@W$zy&r>bq?gi5^CAmUn>v_9EbN)Yu29=8v(OF% z3d-(qkN76 z4altmptIfmyHr;!lHS0@nvZ6^YM8cL=EY% z+Im12bx98{iZf_lW}y=HJYx%!1bC1ME=EBx$#P9^H@L`OUnBAw*q+0K*xK#Y_0Rcg zu3I}can|pa@dw1-!RsGjZ=1T6^(~uXW8Gh>g%Xemll2Y5+{R^V-KbR7ONL!eS01oF z0vQ#E>r!$R#?8RvO@XMPmW=x1$LdToJamw0hLwX*DR3wo5wLKfz!NDHcB7C~n(M2e?1q3f{Lg+bi4f37;;dj&#pMhqnA3KCiHY6= zh^7KN&9OHK51J3S3EQbqU?GA%*V(~LuE1_9Qx@rzuK>#pP)cAtfHD~fgkjv!CREv| z*hJz3B|5Ei>E(`1n=bJP|GF`?+sjL0n|w--aF)QP+Off}%!YYFmO+gT*v&5$0v9So z_$`X{a|hU)oAwsrmn0V92I5v>sXfB0Pa<4Ix#0C93Ks)h@aqp5;({}h@S6>bbQtaD z7c6b_G{ZoDg9ia81rUYe0&El}!bD6lEUR#u9E)xeI46K!g(`8FVwX*)YGGg&j|Yn0 zeg{i#6>2GN;Wpp=73Njv+XkvGl4|)sDG|N_2T#?(H%SiS99;`enHWh#R3e?1wf&{67 z;wLEHl;TZwqM=?nQLh|dy>)#1*T?bSG)Kmp>d3*IFIo{Ks4tK#1X}Tzu!H5J>xb9t zDA`0}d!U0ik8T{^fG|!iD@X4hzFS8#rfneUuEkfZ~f(qT3;P~2pxgoeqt~2Q2pcNvp`UgpNlA< zw7Aw9?Dma45!7b^L73*9ms(tFg^?6}xLX&;no?x{qd$Cfp#1rZZ$A6$Y(x6+nTVuu H|6KSt8{E13 literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/utils.cpython-314.pyc b/backend/app/__pycache__/utils.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47b624634ff2af9e205baacb811e5827ee2cc836 GIT binary patch literal 2071 zcmb^y&u`mg^!x13iPNN6Hkx%*+v3qJBTYlMQFW-BhR`a;#Xk{kjMccCUHPQLL4~3KhUb1T78TYTz2c!p-OP!eU9C-aRG_v`2G5x z-}}Dz-uF_@c6T9wKfiozzDyAMl?yS55wrO!Faa7zDn5_A4#gf0Wx*4G3g^Ywh(`)Y z6?>6NdXbg$BsHl@ASJkDVK1TLepHela;KL{n31aPO;2Eu_{#y}BOY_}7@z>v(75o@ zy9hNww;#QMr?HAPSm}T&?C?VZcorWDu)>IE-1Jz28lF$>yO@ctM|G2q?UHsHpZwqXJ})FJL9z+ZOZfvU29O?ZdPV>*E6Wr&?jaLyQ%4p zqxqg`xi2`kSf*arZM)^^o>8yU3^-4~>-GT(&=!($?h!|yi5r}JA%@Z>tfKJboZ%qB z3#TNT-vPre)^(9(_GU%6~(OcC7K6aSb+<`1ZX3H zGPygWw?{uX9gO{!?!Ghf{>Wl^iMEGV)6WNE4(Lnj}V}*`!_iG!40L(Hn*Dt zKH#3i#`*>tDnQzHTd8xiEVc%?6+q}in(fU%$3(UzHc^}#PKjAEyu8#G*7{b5pKf>HS}gF$Nb@Ex78MSiOP_NgEA_v} z83L2=f_w{1fF38%p1q6aM}_vt{oSSD!aB*Wk)9ROvvlRFv1QV8j~oY+z571C_Tja) z{E?OXk@n15adf3Px|%=r{VVtLXM#%)o;~n+-zR;a?qABb-&rn>Ebl!TTwc%h-OmjM z=i%$F?0e=?&syr>z0|>Wab3>d$=uE?UhlYD%MYyN2Uhcg_vFf!h`I_JGRpQY&Mi(a z>Fr(1g=5R<(;DSbAdg1;=;p{=Gr;kl(G-2=>P{wCAAftgHh(Vb_lhJP_LlF~@{~08CE7;j8CbT%U zs5mC2EVC#l#y>CBr6{v3HO3{iIJ+djASOMtBsDQ6sVFfoMK?FGxFoeGCMhvFJ2fvQ zCb6I(CMB^XF)6V)H6}hjGcU6wK3=b&@)n0pZhlH>PO4oIE6@&*D~dskPt1&rj77{q F767ThFhu|W literal 0 HcmV?d00001 diff --git a/backend/app/database/__pycache__/database.cpython-314.pyc b/backend/app/database/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38fb7ba96f168b78f892dba9693c552c939d5a14 GIT binary patch literal 835 zcmah{y-yTD6o0c{>@CMZKs^c31EKIDQi02|DD816!JJe{RQDrNRMI|= zxB;e^`|G4y!;GX^Mi}$sD8y@oiWZX*&i$y)=0uOH75BCr;FFGI7^IB=6LHq4V>E% znWaJ%XO^%v9ygr2&k1&F6i4305Hn7wQ^W2Wi98248jgoKmID3*6OGN(s1siGYP3&0 z(JeMRFU+10;ale9le5XpzM|>_Uvhnq%^#-q%zR{SS-&4D5ngX%^fEuVj5DZJk{liCFo7KakS#ssr;8|w!^jB zq(N%5J_F6lBW*^Ea76bF9Eb$aUYRzphOyTS$Td1BLMa%V7G_^X2<^bwcPQ;ZVK;N? w`Nrq`_{aSC+sW-rDKWnG4!vr=*FVf{_ufddyIuKDU4w~sXy|Bwo)-Oo0qnrW7ytkO literal 0 HcmV?d00001 diff --git a/backend/app/main.py b/backend/app/main.py index acb583f..aabb977 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,9 +1,23 @@ +import sys +from pathlib import Path + +# Add backend directory to Python path +backend_dir = Path(__file__).resolve().parent.parent +if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +import uvicorn from app.database.database import engine, Base from app.config import settings -from app.routers import auth, users, products, categories, cart, orders, wishlist, contact - +from app.routers import auth, users, products, categories, cart, orders, wishlist, contact, models, brands + +# Create uploads directory if it doesn't exist +uploads_dir = Path("uploads") +uploads_dir.mkdir(exist_ok=True) + # Create tables Base.metadata.create_all(bind=engine) @@ -27,11 +41,16 @@ app.include_router(auth.router) app.include_router(users.router) app.include_router(products.router) app.include_router(categories.router) +app.include_router(models.router) +app.include_router(brands.router) app.include_router(cart.router) app.include_router(orders.router) app.include_router(wishlist.router) app.include_router(contact.router) +# Mount static files for uploads +app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") + @app.get("/") def read_root(): @@ -45,3 +64,7 @@ def read_root(): @app.get("/health") def health_check(): return {"status": "healthy"} + + +if __name__ == "__main__": + uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 16c3ceb..5152d9c 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,6 +1,7 @@ from .user import User from .product import Product from .category import Category +from .model import Model from .cart import Cart, CartItem from .order import Order, OrderItem from .wishlist import Wishlist @@ -10,6 +11,7 @@ __all__ = [ "User", "Product", "Category", + "Model", "Cart", "CartItem", "Order", diff --git a/backend/app/models/__pycache__/__init__.cpython-314.pyc b/backend/app/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c569d7b9a8c0407142959aaad87cf559f347d316 GIT binary patch literal 616 zcmYL`%Wl*#6o&0wCbzjzDshnzMJk)9>O25K-Aotuq700L*jPkvVhvFgoZ_$Pk8P1S3~RbWA2NaXzMx z$N?N2p($FVa}19c$uY{aJ41zXtb>nANV7-pWmm0BFgBiX5X(+p8y{YDm1uHL2Qx0g zev&{ztV}1I%SuR{xXty<)laqh($vbvjAva7yo3v(6km#Wc`AJ~QwCN-D+4Q$mDoyR z#Xg(NtPG9hhdNjrKzCC|{f^N^=(yZtx-cECEUC-XL1{1_l6BZw)Q15&{<`OMqx7WQ zp%$whtDCK(qTo$a6gTMJpdkHW_J-NlQmoq4vSIU9oXdJ6*tt+w(DiItgW&8^^0qo% zaRoxMOI}`ywqm^RnFCa;DC)L`qL}vAI(}=K>qeZ(=VpTaCF-@2-#*58i=J%Ji!FM& YMbB>0t6OyJ{K=mn%e?QAd}LEW%REsTh#rAq|=wjIgG+O@>K!8i#Bq$;@mqw;V(~ zDqcMH>aBl*|A7cS=zxfL@+PEsb|#zPgL(7beDmIX-=t4209>u4N!*?)-7q(ZE5d;julxAr|wF34{U>d5ZOB*a@y{Zo!3e)a_@rB`)$(D zR^G`d&4x_*4JJvSaOFLhoU(ohl~cun^zyRY`^#kB(!mV0frJr|R>zTc1f_muAGEzl zGl=ZU>OFsP8tj8ka-{Vb)3~FTTXFJS79vj;BVY3hlw0MPCCcrZtrc3TH0E@O`4kPv zRJoKTSJY#f{5f!j3O>s9RyrjE*1kJFd0)!YI5`QQwwNd!WCUEi4N1yu9o zVSFkG7jbebw+PlD5w+TckZ7|l0ahljhz~h6u6@^^wd455MQ0u|~RnyF`Lwk}e!o};-b?#nc!x=iIn z&o-30LX|`p8~M6Q)hRTFrjd~wM~2i?tJyR+)e@@=R%OUqF!FDqiuwl+s^plo82WK5 zU~(g%cnuRKJ)p!3)|h%HB-mREKE>Niso4}Sdo5fMSb<>Qrd}9C>s}kyiVHrP`osl0 z!cO}5R7&TI)1N` zBtmU!_b;92AG#URd?sDeVpE44vLP7Cz*b6Zd9bO5HpqCvD9{n32$Rv7xM&l~L!C*?>U#kWLU0!?i`uI`PW^<>l>mTvLXuG?|>UPS&gAD%OaN{@D#p?-CMk6 z^7bRs-SUWUejeZ&@wTv8!_fu}+vXZ1vdvXu2k!Nj9Z^h7J|-NvrrmCv&?ELElMjEk zy-f;`GZDPd=mCfy&~Kv?oqNyorCoV{`DnD8pX)62^wHfffBf#?^6}&czt+0?$DO;q zk?}p{;My-Y4sUcvZgoC+u8r?W2U}fju2bt3F6~Vm-FaI3s$2N_@oh5Gf6A-beG2X& zcFX^l-D;o5lv^nwJJ%>#Jkm-QbHlrjZq52#1C2|qWP z78aBY3jc~I@YTxM)FS+2cI2J z-#q3V^-5#AYx|qsQl(SxX(PKc`_-;?jWbo=o!hT<3zg38l(k1)Z656U#NN##v#T#W zzMIy=OIfNEnHp{Sw(qRt*7ndSc!$X-l6RBo{{+p3q?NXtys%^Ny3E4iYp=qgJ5UJ! z&q=o@Y5Ct{V9JN-A@UKKgE)S+M(>08Gb;$f@2K_+)lSgW6LjSS&74V!@Qv`KdWQJ@ R?4l+t3Qy=?h~JY&{stoEwMqa0 literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/category.cpython-314.pyc b/backend/app/models/__pycache__/category.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccd0420263ca549f5e7bb61c0e7cde17347b796c GIT binary patch literal 849 zcmZuw&1(}u6rcUf=A$i^Rz;zuD21&UL8JvK9@+>AV#T%IwjCzfF%H?Cq%*U{+;S-5 zG2T4(Sa1Fn-h~|OfQWeVCbW3=Z8oVOonhYo-tX}~XZFtQObNlNem-aK4TOGX%5*ss zaJC6xfc8;fG*I18M%`3q-BQ+a#+ePf?kHy&Eus|^Sl3Ws?>P^w+*i4Qa|`+IuT1qV z?QVDDG~v3?;EJ}X(C!N*7;pQAwzr6+t=uh_?nF5|0x&>(NErcA=Dwq>z)+B71?{@FOtc0|w zUc^``qF4(P1+#Caib{;dEcl+mLHhaj!|H6LI*A)0$w1G^|nCQQcnHW{Q%q zDF~15CPXSK!X{~TD33zY>xIw?jb%6;Pqo(<9>g7stU}oU@fLkuSQdEcV{Jp`G)5`pC{kS%&_(zY$jcM#1zo1L< zASQ9^HBI{eW|Pll5o)VO1f~b?>;j1Fk>&Me6Ud|81f;#q{7xQ;B~antKr&19+cFH} XJF0#`)o*C^+$|Vu@6;azpRw;>q-NiY literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/contact_message.cpython-314.pyc b/backend/app/models/__pycache__/contact_message.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe230bc18650ec77a7bd3c5f381a3fb40702b368 GIT binary patch literal 973 zcmZ8f&rcIU6rSDgueQ)ygm6McBe5~Ug+PL)2dJ?%0m)j8hb`lDcc?47+cNu#^wxv% z(o2sVJ<3T>{w0{mB*YlKcr$@`c6PUg$ZYoQ_q{j!^4|B}z1(;PiSz5#x%)*z=x0Pm zM;(H*EdhFH8<|oSRV0uqGRTz_q$&z16&2LkC?;36iUwMYQ&qiUfH8}v&>S+AX=JK( zV=0y7gS4sLMn(O1M2jld)>~f354c_pfHo=P+B0Bo(Bwv$0Ij(`@}7|@6mzu6`r}m zvoRmLxIZ5ATs^;aGfwcIF_jfLaX`UTVIt|9=hj;R5F0jV7?LLCc{}0QPaL(P%(HFg z`h<1yF70yF4IFwPde$AfLpmNDq0QoTZeSb`pJL2282c@!<3)H3tUzyhT6dND5MW7WRcX&h+J9+ z?4b3QXKY5ra2!m4PhlJa0oN8pjIz-I8e+_@iTh{bMf~OZs`WgiEVP_`mwDFHfR>rN zPpvWyccIm`nl4acH5dt;`#uSQGOI!CT^cwRX}2xWEcHU`if#+-F3XDav56`StHQiT z-zM+$Hh!e@1N}qo^W<@Qxwm$bEez)SrC#}DY+|t7-|Rg)$xRMk^n>2x2-NzX0EOv8 zaU!aa|8_G{{QFGY+6OvH)wN)(zB6)%XzB@MB#7l5Tl z*!ZjfcrBOYD}Q(PD-(!u&}|lJp&wzM|48nmI-D=b9ls7}WkE@ri%`2N}Bl ASO5S3 literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/model.cpython-314.pyc b/backend/app/models/__pycache__/model.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06324835821f76ed1ba4563479499ed6b6b24956 GIT binary patch literal 1442 zcmZux&2Jk;6rZ)dA6~ERIBl9jKvhW9Vye0bMJZ6!$hA|gQ=CK_b11SiY`hb9mG!PW zJ1(Rr9}wc$suv>;gj;$de*!1YAX$Qp6d}Zk8z>c5c(ZFKRAMa8y!pL1@9Q_S*K%1M z!6^Ok!o8nF=y$Q140$Y^I3Ntr7P6!oG9@ON36?OEENP}#%9NRGDoim|rd|>@iJE4n zSvr!FHQm&i9?7X%#x$653C*B+WXZG0Qd-6$#1w;NEp-7EwLiqBsB)zec>S)=)tb*p zhfuC;GV1yru2xqnwR-tOuHOwPaXbD!vd7bnT76}+QLb;z#$%&yJBe+6Fk$SZr7%Jco*oAUEd*}-4!hOeb2MEJ@Poy20l6g-7C2cS1aYs zO1ZisECR6g2opT14%xB$9^+bNY|ND?LMW8mc6*nS9jh z?}Dd?vpBYACYF2gu5*0H9IaNb~8%%de;sap-s_0;yX>d*K5X|j+atzkLJK> zG>MwSZ(#ZY{c?V8Q2#BRf1>U;4$qI$OM^Q{nZna6!*kEB9mu0hX;3|yzBruy_N{}~ z_xDF{tpE5i9!-BTxOZesKV2FwJiB!;Gcs-rR*nj@U#a_v{qom2@KabCtb)ap#r?}8 z<0@#u$AyE-qv;!PvM@VLAH4hB^7G|U;nraFF9l7#He5Z_M)~&!=23obSUc>D^0z-X z>Ges*YsDPb!-t;jwI7h~-Wi<{l@V7FlQ9<&Q@JzqQb4;A4>3zK(3}!{O>9B#sX&cM zu!w>jSiZ4aD0MnmINs@j=jh|8DC)7AbSC7%_$LPunB_}O^S^XQq H5;^%7RB(HE literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/order.cpython-314.pyc b/backend/app/models/__pycache__/order.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..979fb28148fe2370bd55a9af30fb6edd4c863340 GIT binary patch literal 2254 zcmbtV%WoS+7~l0{cWtljIB{?uZPS1VHPDntn$ik-Xx-HABdM04BCxw`cBkpm^{zWR zTk0IZhYBI}VyPEbT*56m5&i=>ajg_f;1Lz7;KB{55#q=0N?tcb&$U9M2$BDH#N<+I2*eOCCn{x_cfqiy~Dqatva7 zu7AyL0IgV{M?eqGu~UYR%+QN)l^zq1-WOTOHGznlKq6OU64gW!tHo!9nxsYGFM6!+ zz8rFE3WOG0fP3s)h=e;yaA}g?i4X3i9&;s4KGxS%IWOI}Kr*4ufX+Tck32)?o}ouM zJyy$KdO7rEcpZB8x&PApm)pu9l0t0);!bEuEj5T_lJite)Y4jJpgE;w$+R{CMa|Lp z3bvqzagS}Q+Yy>-V7qSMrhXkYY20=#bZbKne1zw1s~DkD;Mz9=gmVxBl^e*lp!85d z*+N&1z#+v5&NGqXxj}<6aGX~U zTF5~J9rkeJn&E=IN&b{+_@-f@ZDF~%k1D$E8g-=WG^y)#&k7uNR(1VGU^tH@SzW(s zW1l#-i(C)HXFj1;DKsG!uSefi$UTa&v7-rUVBw9fX% zCOdOm`JL77*ooGKUV5y(v{~t<=UYom^5WfhfLYkzxqA20`>}j4Kh;^hyWG0io1WdA z`|{-d*u;=9SLi5PhrT|teWaT^(K^?ALVsKSe&xHBZf>=8?qRCXk+yDiQ*X6OJ$0fp zy>t2g(g$7j!`o-@>-`x1dG`DS;-T3y^*?8i#3xg{KSu_WDamm!_4y>6N$P-3b9n|P zmoz|jID?+%jnr~50c1_l5(B+a&V!jS#2eF8lGoCJ{|ugj5NR^(bk<0#I18K&B`mXT zZ6q5Q@MRJPe;C?s;5ZCKVfl}+{7odHdG{?B$MJrqcYrN3Y&po5S+)#7 zh@WR9M`8Fjz>xOeW@iPU?Giv!h`>0EDHDZ2jrsNlqH)u6Jj{`SGhhs+Q|J7h7f4WAay1TNid`kMHuPV;Xlp z1Bp7?K6>Y3>r5||YtL;ibW?{|!;Q8VH%nc$*gEqNny;`^=&Hv-Co|PKzEkUFPTxKo zdd2)$E@o)bzu_2;c@5Q@L)l>ii3c9e2%e|R6U})K*Tcy9^cV-c&cpCE*5GG#aKI^M zgu|9c|8z#E@^&~#PvTRc$9_G1mbl-eqA30>oc}>M|3G-@fw2F9Ft;a-h*j~6(w@Nf Ry*X7ZiS5`Q0^4~se*qK(J0t)A literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/product.cpython-314.pyc b/backend/app/models/__pycache__/product.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6cedc7f97484928d3985de9d2062c474366fc66 GIT binary patch literal 2048 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 Dy(BGR literal 0 HcmV?d00001 diff --git a/backend/app/models/brand.py b/backend/app/models/brand.py new file mode 100644 index 0000000..8006783 --- /dev/null +++ b/backend/app/models/brand.py @@ -0,0 +1,8 @@ +from sqlalchemy import Column, Integer, String +from ..database.database import Base + +class Brand(Base): + __tablename__ = "brand" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), unique=True, nullable=False, index=True) diff --git a/backend/app/models/category.py b/backend/app/models/category.py index 73ce1ef..32ae02d 100644 --- a/backend/app/models/category.py +++ b/backend/app/models/category.py @@ -9,3 +9,4 @@ class Category(Base): name = Column(String, unique=True, index=True) slug = Column(String, unique=True, index=True) description = Column(String, nullable=True) + image = Column(String, nullable=True) diff --git a/backend/app/models/model.py b/backend/app/models/model.py new file mode 100644 index 0000000..05f1aa7 --- /dev/null +++ b/backend/app/models/model.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String, DECIMAL, ForeignKey, TIMESTAMP, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +from app.database.database import Base + + +class Model(Base): + __tablename__ = "model" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), nullable=False) + category_id = Column(Integer, ForeignKey("category.id", ondelete="CASCADE"), nullable=False) + brand = Column(String(100), nullable=False) + base_price = Column(DECIMAL(10, 2), nullable=True) + sizes = Column(JSON, nullable=True) + description = Column(String, nullable=True) + created_at = Column(TIMESTAMP, default=datetime.utcnow) + + # Relationships + category = relationship("Category", backref="models") + products = relationship("Product", back_populates="model") diff --git a/backend/app/models/product.py b/backend/app/models/product.py index fd79036..e068d9a 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON +from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, JSON, DECIMAL from sqlalchemy.orm import relationship from datetime import datetime from app.database.database import Base @@ -9,10 +9,12 @@ class Product(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) + slug = Column(String, unique=True, index=True, nullable=True) description = Column(Text) price = Column(Float) discount_price = Column(Float, nullable=True) category_id = Column(Integer, ForeignKey("category.id")) + model_id = Column(Integer, ForeignKey("model.id", ondelete="SET NULL"), nullable=True) gender = Column(String) # men, women brand = Column(String) sizes = Column(JSON) # ["S", "M", "L", "XL", ...] @@ -21,8 +23,11 @@ class Product(Base): images = Column(JSON) # Array of image URLs is_featured = Column(Boolean, default=False) is_on_sale = Column(Boolean, default=False) + override_price = Column(DECIMAL(10, 2), nullable=True) + override_sizes = Column(JSON, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) category = relationship("Category") + model = relationship("Model", back_populates="products") cart_items = relationship("CartItem", back_populates="product") order_items = relationship("OrderItem", back_populates="product") diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 2678d2e..2d0c403 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -24,6 +24,7 @@ class User(Base): postal_code = Column(String, nullable=True) country = Column(String, nullable=True) is_active = Column(Boolean, default=True) + is_admin = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) cart = relationship("Cart", back_populates="user", uselist=False) diff --git a/backend/app/routers/__pycache__/__init__.cpython-314.pyc b/backend/app/routers/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf08c6302866d892351f4d5a178993f0d89dc0c3 GIT binary patch literal 181 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08CE6CX@CbT%U zs5mC2EVC#l#y>CBr6{v3HO3{iIJ+djASOMtBsDQ6sVFfoMK?FGxFoeGCMhvFJ2fvQ zCb6I(rYOG@C{!F1AD@|*SrQ+wS5SG2!zMRBr8Fniu80+A1;`P_AjT(VMn=XWW*`dy De=IN~ literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/auth.cpython-314.pyc b/backend/app/routers/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fe2a58304bbcfc8653853af801f4ade695e0759 GIT binary patch literal 3639 zcmb7G-ES1h5%2lf*$?lIKh}5+_JR34_K?>Q3|t6Y!ePTMiC`IEb4rFAjd#ZO-0j!( zv;p&yB|?JJJ={e>3VBJ0gcPHkl#B9^`wQ}nVf2|-5+J^3U{3R)|q@;cM=y{uQvqlLbFKkLsAuz`G< zrSpSqP^`xaoAN_!XdAgKZ6`+DNaQ4==ZGBW#WI&O#RH+T-O)&1?ln>&*JXJ-xty4U z8(=#q8G42oy%|#Jt4Nco@8(--TcH+Y4;otue0*yVe41 z@^o3ST6Vb%f3R5neYeK@QEJ2X{i}8ASo5|&&y!bN>dh_&4Dh>{?=t4q=KSmN3A^D< zpk%IM4?1WK>dt!CxXNt7K;1ddhl|2Fown7ns&%+T86T_tU_=F4fp2X;bWKts@5*o9 zB*cNWQF30Imkh~-ouC=A*>(;>CS)+I@+Ho>Dy}7iO=Q{w3p5HHT57gVt2XoTXFK@W zlSfT7j&C|wJX$f&)!ZrSU2)AR*MFbY8|IwHT-%(XcFh^D+NiuaW0%1=j%ha6g-9VRz@~Dh~ILh}jzKl5*!3Fwx)> z7Wf?01Pe&T&^E~0Zryu_&Q=WJ8?wOInMC8M7+@E@yEp^ncfR zAoHF8)K(e%pKFfGfHSM`h>J&fe0HHyv1)eJ<-G_z*XaN;tMkYQ3oc!x7{|e(y$ZM` z-GP(HtVexD(GMs-Hy^}EVomPfZ@qnD$}*ON zVIS2V>pPeAo%g4Hd*(N1KHK%DH~#R(@~&em`tis5$z}cIiay;u{Vb|K(MO&PZ+$#` zaC!J(D?Pp*QF;@rM2RQX6C|^DN&S85L26~_aP!;~eW0ZeeEi;r@3jVoev$iGt~LDn zdQ|Pznx|J2Boe)K>c*+tM{b^LsoJf=jl#|EBXQ=&ncG(%shL*KK=Ukp4OG0YO;qZ0 zUTs4N>HzSVxWxYqLBJI>L+Ju;LV<9kQgk-)Iy8$Eni6880?*WrNfg6F5Rim1McfeG zUfkFYT@XAp3>Vs+GzJgtz9NQCGh$I(rQAUW9D&px#E?D_LolHS?T2F}X);8EQc^+$ z&jkqX$T0DRMSEF{cmctkC}E1?0*@6t5g?F)J>amdM?*Ey3q=E?bt8cCO%Y&1DyfDV zMvg|rh?F9RW<-sc*};8rD3seBx08|*;#)jR{Cc`jyJA;7XACl*gUQgY_;ec_fg};F zdNs(%KD^QbBoMATHnVxkz1Hvm6_&`lJh>rPCPd1#*kS|FJmcCFAc@e)CTR+W=ODSM z4ao!6rL!k4UCf^|3O|~jqWE4E&A>;V7i(6cy=yGRR(cLJr=Ionh6tTp z9-eHa_ijMw=1j@ zy>@Mv(Ds|bIw1ilWdTYa5wfz9uqO|-WJ97d&yudurhxg3T7c^z~C6GmU5 z&x|TGpitKn>9=7Dw)h^dz73s^^7ph|Vbk-Ko&^O@qwduhodOmNivD+?1J}^E+S zZ*QJ%MfIQlWTR{B=(@&gjHHG>xq2_UH2AP5_fX9Vvz^V3^5`s-RZu!oG#OQ~>!vT= z|Da-5%JXh@F$WGV0w#|E8eOb@1!njrD8zCI&>7ozI|Rju#1ptDDoj7fmVLP3qd?ow zp@D+pP~fOv?78%c2Q_dGOS%BM0&*Cvg#7}U<$7(_n+x!&1yvwd8+8cknEwQTgsC17 zanW{?JO-*PRNS{{5tz_r{Cm)?Dv~5UBga?B@uy_RQ?m6b+541?JR^s`mLrn9Mxgsw zl1O8Jix1r0w^UlnKZ+k}YO5NNW4A_bjNJb5(r9yJMLPV&_~d7$-&z00+Hy@H@{v^; usMWn8-#aVP&=&`fet!6m-$fn`c{0eOb$P56>9utk{2Dj6{u3#2gZ}^`SLw(A literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/brands.cpython-314.pyc b/backend/app/routers/__pycache__/brands.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27900a3d23a48fddc9f14a34e7bb26a0a0c7aa54 GIT binary patch literal 5748 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 GrTiCvY+9@U literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/contact.cpython-314.pyc b/backend/app/routers/__pycache__/contact.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..906e5871148358ca7bb4808626e45414a5ec4022 GIT binary patch literal 1296 zcmZ8g&2QXP5T9pz?fne>N+Jko+n~0rC>s#{QmK$2A$S8#x2ELe&GOcMNv!U!&3ms^ zTP_=^Qu8Np-~i$PTo8W%H!d7okwbY>R3UolEf54(=GopV=}4aa&4*_^znOh~qE-f! zC!hTjtr`G-YGW*g9PIswU<9k+kruc_64#K1Yf7`OeMZZ23$l=6vt_%EbW&`!if&1k zQe0@2-HNQZRavb=hnxV<_M9g1iWjh_7Fp}m+OK4IzxGOJ3dZO`Kz z<>Ua%*a>HA(QRc(x-JyxRy=>HJBUNp5A=ljlwct?@!5M5)d*H#)wp;MAVlBGaE)vb zkN6nL#54SyQGhuEc`%=9TRDSl2xZ`C1}~wH+o-U&y%zHUl|uKMRoCV(`JW2Lg&*FE zc;CM|WD7jH#ry>(Zpb+C*CWZO-{W)`zA>NzdG&iVxPiR-G)a7(QZ4*^+GcV`Rl8j} z9L5s4>UQ}QwyEGFW>AgbKPZr?zqHz(J-@4efBrBv_b?2>vtiKC->wTtBB>l)KQ4TuVe%uG!jI~_a@gzUOA>mLQ>TveNgX&QklSv~=QxGe zF{J;w;xeicJULu{c=)ZaPk(pjn={`w5ssD~S@!+K&Bd)t+t!rEtA&X+PLtgF8*Ul=N)WB<{KyQb~JM zuxEg4@>E&J(mNIz1xK|*=n7Oc^Fgu`)DtK>yBTFAv5@It{6*bee^^FA@iLaX*=O(a z)9BNC7t5%2O+v_HIQJ0F?ZE5~ytV@;AH&Cgl#YDa{HgT)7u%(GM)s}^M)Cg4=FFD& h;Q0B`%x~nv?~7M{UHc{eS6|zn3C6|04Mb_b{{iWBFGm0X literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/models.cpython-314.pyc b/backend/app/routers/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74556f0ef298dda11420e9961e2646c92d1c64c2 GIT binary patch literal 6660 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 literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/products.cpython-314.pyc b/backend/app/routers/__pycache__/products.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad066b655b200d9258b47e512b178fa89c85b3e8 GIT binary patch literal 11345 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 literal 0 HcmV?d00001 diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 8aebd1f..a70ab97 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -47,7 +47,7 @@ def login(email: str, password: str, db: Session = Depends(get_db)): access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) access_token = create_access_token( - data={"sub": user.id}, expires_delta=access_token_expires + data={"sub": str(user.id)}, expires_delta=access_token_expires ) return { diff --git a/backend/app/routers/brands.py b/backend/app/routers/brands.py new file mode 100644 index 0000000..c8a9314 --- /dev/null +++ b/backend/app/routers/brands.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from ..database.database import get_db +from ..models.brand import Brand +from ..models.user import User +from ..schemas.brand import BrandCreate, BrandUpdate, BrandResponse +from ..services.auth import get_current_user + +router = APIRouter(prefix="/api/brands", tags=["brands"]) + +@router.get("", response_model=List[BrandResponse]) +def list_brands(db: Session = Depends(get_db)): + """Get all brands""" + brands = db.query(Brand).order_by(Brand.name).all() + return brands + +@router.get("/{brand_id}", response_model=BrandResponse) +def get_brand(brand_id: int, db: Session = Depends(get_db)): + """Get a specific brand""" + brand = db.query(Brand).filter(Brand.id == brand_id).first() + if not brand: + raise HTTPException(status_code=404, detail="Brand not found") + return brand + +@router.post("", response_model=BrandResponse) +def create_brand( + brand_data: BrandCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Create a new brand (admin only)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + + # Check if brand already exists + existing = db.query(Brand).filter(Brand.name == brand_data.name).first() + if existing: + raise HTTPException(status_code=400, detail="Brand already exists") + + brand = Brand(name=brand_data.name) + db.add(brand) + db.commit() + db.refresh(brand) + return brand + +@router.put("/{brand_id}", response_model=BrandResponse) +def update_brand( + brand_id: int, + brand_data: BrandUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Update a brand (admin only)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + + brand = db.query(Brand).filter(Brand.id == brand_id).first() + if not brand: + raise HTTPException(status_code=404, detail="Brand not found") + + # Check if new name conflicts with existing brand + if brand_data.name != brand.name: + existing = db.query(Brand).filter(Brand.name == brand_data.name).first() + if existing: + raise HTTPException(status_code=400, detail="Brand name already exists") + + brand.name = brand_data.name + db.commit() + db.refresh(brand) + return brand + +@router.delete("/{brand_id}") +def delete_brand( + brand_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Delete a brand (admin only)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + + brand = db.query(Brand).filter(Brand.id == brand_id).first() + if not brand: + raise HTTPException(status_code=404, detail="Brand not found") + + db.delete(brand) + db.commit() + return {"message": "Brand deleted successfully"} diff --git a/backend/app/routers/cart.py b/backend/app/routers/cart.py index 4ab6d38..66f6b95 100644 --- a/backend/app/routers/cart.py +++ b/backend/app/routers/cart.py @@ -9,42 +9,40 @@ from app.services.cart import ( remove_from_cart, clear_cart, ) -from app.services.auth import verify_token +from app.services.auth import get_current_user +from app.models import User router = APIRouter(prefix="/api/cart", tags=["cart"]) -def get_user_id_from_token(token: str) -> int: - user_id = verify_token(token) - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", - ) - return user_id - - @router.get("", response_model=CartResponse) -def get_user_cart(token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - cart = get_cart(db, user_id) +def get_user_cart( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + cart = get_cart(db, current_user.id) if not cart: raise HTTPException(status_code=404, detail="Cart not found") return cart @router.post("/add", response_model=dict) -def add_item_to_cart(token: str, item: CartItemCreate, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - cart_item = add_to_cart(db, user_id, item) +def add_item_to_cart( + item: CartItemCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + cart_item = add_to_cart(db, current_user.id, item) return {"message": "Item added to cart", "item_id": cart_item.id} @router.put("/{cart_item_id}", response_model=dict) def update_item( - cart_item_id: int, token: str, update: CartItemUpdate, db: Session = Depends(get_db) + cart_item_id: int, + update: CartItemUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), ): - user_id = get_user_id_from_token(token) cart_item = update_cart_item(db, cart_item_id, update) if not cart_item: raise HTTPException(status_code=404, detail="Cart item not found") @@ -52,16 +50,21 @@ def update_item( @router.delete("/{cart_item_id}") -def remove_item(cart_item_id: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) +def remove_item( + cart_item_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): if not remove_from_cart(db, cart_item_id): raise HTTPException(status_code=404, detail="Cart item not found") return {"message": "Item removed from cart"} @router.delete("") -def clear_user_cart(token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - if not clear_cart(db, user_id): +def clear_user_cart( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + if not clear_cart(db, current_user.id): raise HTTPException(status_code=404, detail="Cart not found") return {"message": "Cart cleared"} diff --git a/backend/app/routers/categories.py b/backend/app/routers/categories.py index 898f384..ef8cb73 100644 --- a/backend/app/routers/categories.py +++ b/backend/app/routers/categories.py @@ -2,8 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from app.database.database import get_db -from app.models import Category +from app.models import Category, User from app.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate +from app.services.auth import get_current_admin_user router = APIRouter(prefix="/api/categories", tags=["categories"]) @@ -22,8 +23,11 @@ def get_category(category_id: int, db: Session = Depends(get_db)): @router.post("", response_model=CategoryResponse) -def create_category(category: CategoryCreate, db: Session = Depends(get_db)): - # TODO: Add admin check +def create_category( + category: CategoryCreate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) +): db_category = Category(**category.dict()) db.add(db_category) db.commit() @@ -33,9 +37,11 @@ def create_category(category: CategoryCreate, db: Session = Depends(get_db)): @router.put("/{category_id}", response_model=CategoryResponse) def update_category( - category_id: int, category_update: CategoryUpdate, db: Session = Depends(get_db) + category_id: int, + category_update: CategoryUpdate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) ): - # TODO: Add admin check category = db.query(Category).filter(Category.id == category_id).first() if not category: raise HTTPException(status_code=404, detail="Category not found") @@ -49,8 +55,11 @@ def update_category( @router.delete("/{category_id}") -def delete_category(category_id: int, db: Session = Depends(get_db)): - # TODO: Add admin check +def delete_category( + category_id: int, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) +): category = db.query(Category).filter(Category.id == category_id).first() if not category: raise HTTPException(status_code=404, detail="Category not found") diff --git a/backend/app/routers/models.py b/backend/app/routers/models.py new file mode 100644 index 0000000..6ee78d4 --- /dev/null +++ b/backend/app/routers/models.py @@ -0,0 +1,105 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from app.database.database import get_db +from app.models import Model, User +from app.schemas.model import ModelCreate, ModelUpdate, ModelResponse +from app.services.auth import get_current_admin_user + +router = APIRouter(prefix="/api/models", tags=["models"]) + + +@router.get("", response_model=List[ModelResponse]) +def get_models( + category_id: Optional[int] = None, + brand: Optional[str] = None, + db: Session = Depends(get_db) +): + """Get all models with optional filtering by category and brand""" + query = db.query(Model) + + if category_id: + query = query.filter(Model.category_id == category_id) + if brand: + query = query.filter(Model.brand.ilike(f"%{brand}%")) + + return query.all() + + +@router.get("/{model_id}", response_model=ModelResponse) +def get_model(model_id: int, db: Session = Depends(get_db)): + """Get a specific model by ID""" + model = db.query(Model).filter(Model.id == model_id).first() + if not model: + raise HTTPException(status_code=404, detail="Model not found") + return model + + +@router.post("", response_model=ModelResponse) +def create_model( + model_data: ModelCreate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) +): + """Create a new model (admin only)""" + # Check if model with same name, category, and brand already exists + existing = db.query(Model).filter( + Model.name == model_data.name, + Model.category_id == model_data.category_id, + Model.brand == model_data.brand + ).first() + + if existing: + raise HTTPException( + status_code=400, + detail=f"Model '{model_data.name}' already exists for brand '{model_data.brand}' in this category" + ) + + db_model = Model(**model_data.dict()) + db.add(db_model) + db.commit() + db.refresh(db_model) + return db_model + + +@router.put("/{model_id}", response_model=ModelResponse) +def update_model( + model_id: int, + model_update: ModelUpdate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) +): + """Update a model (admin only)""" + model = db.query(Model).filter(Model.id == model_id).first() + if not model: + raise HTTPException(status_code=404, detail="Model not found") + + for field, value in model_update.dict(exclude_unset=True).items(): + setattr(model, field, value) + + db.commit() + db.refresh(model) + return model + + +@router.delete("/{model_id}") +def delete_model( + model_id: int, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user) +): + """Delete a model (admin only)""" + model = db.query(Model).filter(Model.id == model_id).first() + if not model: + raise HTTPException(status_code=404, detail="Model not found") + + db.delete(model) + db.commit() + return {"message": "Model deleted successfully"} + + +@router.get("/brands/list") +def get_brands(db: Session = Depends(get_db)): + """Get list of unique brands""" + brands = db.query(Model.brand).distinct().all() + return [brand[0] for brand in brands if brand[0]] diff --git a/backend/app/routers/orders.py b/backend/app/routers/orders.py index a3de381..446ed80 100644 --- a/backend/app/routers/orders.py +++ b/backend/app/routers/orders.py @@ -9,25 +9,19 @@ from app.services.order import ( get_user_orders, update_order_status, ) -from app.services.auth import verify_token +from app.services.auth import get_current_user +from app.models import User router = APIRouter(prefix="/api/orders", tags=["orders"]) -def get_user_id_from_token(token: str) -> int: - user_id = verify_token(token) - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", - ) - return user_id - - @router.post("", response_model=OrderResponse) -def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - order = create_order(db, user_id, order_data) +def create_new_order( + order_data: OrderCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + order = create_order(db, current_user.id, order_data) if not order: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -36,16 +30,21 @@ def create_new_order(token: str, order_data: OrderCreate, db: Session = Depends( return order -@router.get("/user/orders", response_model=List[OrderResponse]) -def get_user_order_history(token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - return get_user_orders(db, user_id) +@router.get("", response_model=List[OrderResponse]) +def get_user_order_history( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + return get_user_orders(db, current_user.id) @router.get("/{order_id}", response_model=OrderResponse) -def get_order(order_id: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) +def get_order( + order_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): order = get_order_by_id(db, order_id) - if not order or order.user_id != user_id: + if not order or order.user_id != current_user.id: raise HTTPException(status_code=404, detail="Order not found") return order diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py index 1915c1b..db359bc 100644 --- a/backend/app/routers/products.py +++ b/backend/app/routers/products.py @@ -1,8 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File +from fastapi.responses import FileResponse from sqlalchemy.orm import Session from typing import List, Optional +from pathlib import Path from app.database.database import get_db -from app.models import Product, Category +from app.models import Product, Category, User from app.schemas.product import ( ProductCreate, ProductResponse, @@ -16,6 +18,8 @@ from app.services.product import ( delete_product, search_products, ) +from app.services.auth import get_current_admin_user +from app.utils import save_upload_file, generate_slug router = APIRouter(prefix="/api/products", tags=["products"]) @@ -23,22 +27,37 @@ router = APIRouter(prefix="/api/products", tags=["products"]) @router.get("", response_model=List[ProductResponse]) def list_products( category_id: Optional[int] = None, + model_id: Optional[int] = None, + brand: Optional[str] = None, gender: Optional[str] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None, on_sale: Optional[bool] = None, featured: Optional[bool] = None, skip: int = 0, limit: int = 20, db: Session = Depends(get_db), ): - return get_products( - db, - category_id=category_id, - gender=gender, - on_sale=on_sale, - featured=featured, - skip=skip, - limit=limit, - ) + query = db.query(Product) + + if category_id: + query = query.filter(Product.category_id == category_id) + if model_id: + query = query.filter(Product.model_id == model_id) + if brand: + query = query.filter(Product.brand.ilike(f"%{brand}%")) + if gender: + query = query.filter(Product.gender == gender) + if min_price is not None: + query = query.filter(Product.price >= min_price) + if max_price is not None: + query = query.filter(Product.price <= max_price) + if on_sale is not None: + query = query.filter(Product.is_on_sale == on_sale) + if featured is not None: + query = query.filter(Product.is_featured == featured) + + return query.offset(skip).limit(limit).all() @router.get("/search", response_model=List[ProductResponse]) @@ -55,16 +74,32 @@ def get_product(product_id: int, db: Session = Depends(get_db)): @router.post("", response_model=ProductResponse) -def create_new_product(product: ProductCreate, db: Session = Depends(get_db)): - # TODO: Add admin check +def create_new_product( + product: ProductCreate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user), +): + # Auto-generate slug if not provided + if not product.slug: + base_slug = generate_slug(f"{product.brand} {product.name}") + # Check if slug exists and make it unique + slug = base_slug + counter = 1 + while db.query(Product).filter(Product.slug == slug).first(): + slug = f"{base_slug}-{counter}" + counter += 1 + product.slug = slug + return create_product(db, product) @router.put("/{product_id}", response_model=ProductResponse) def update_existing_product( - product_id: int, product_update: ProductUpdate, db: Session = Depends(get_db) + product_id: int, + product_update: ProductUpdate, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user), ): - # TODO: Add admin check product = update_product(db, product_id, product_update) if not product: raise HTTPException(status_code=404, detail="Product not found") @@ -72,8 +107,82 @@ def update_existing_product( @router.delete("/{product_id}") -def delete_existing_product(product_id: int, db: Session = Depends(get_db)): - # TODO: Add admin check +def delete_existing_product( + product_id: int, + db: Session = Depends(get_db), + admin: User = Depends(get_current_admin_user), +): if not delete_product(db, product_id): raise HTTPException(status_code=404, detail="Product not found") return {"message": "Product deleted successfully"} + + +@router.post("/upload-image") +async def upload_product_image( + file: UploadFile = File(...), + admin: User = Depends(get_current_admin_user), +): + """Upload a product image and return the URL""" + # Validate file type + allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"] + if file.content_type not in allowed_types: + raise HTTPException( + status_code=400, + detail=f"File type {file.content_type} not allowed. Allowed types: {', '.join(allowed_types)}" + ) + + try: + # Save file and get path + file_path = save_upload_file(file, folder="products") + # Return full URL + return {"url": file_path, "message": "Image uploaded successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error uploading file: {str(e)}") + + +@router.post("/upload-images") +async def upload_multiple_images( + files: List[UploadFile] = File(...), + admin: User = Depends(get_current_admin_user), +): + """Upload multiple product images and return the URLs""" + allowed_types = ["image/jpeg", "image/png", "image/jpg", "image/webp", "image/gif"] + uploaded_urls = [] + errors = [] + + for file in files: + if file.content_type not in allowed_types: + errors.append(f"File {file.filename}: type {file.content_type} not allowed") + continue + + try: + file_path = save_upload_file(file, folder="products") + uploaded_urls.append(file_path) + except Exception as e: + errors.append(f"File {file.filename}: {str(e)}") + + return { + "urls": uploaded_urls, + "uploaded_count": len(uploaded_urls), + "errors": errors if errors else None, + "message": f"Successfully uploaded {len(uploaded_urls)} of {len(files)} images" + } + + +@router.get("/filters/brands") +def get_brands(db: Session = Depends(get_db)): + """Get list of unique brands from products""" + brands = db.query(Product.brand).distinct().filter(Product.brand.isnot(None)).all() + return [brand[0] for brand in brands if brand[0]] + + +@router.get("/filters/sizes") +def get_sizes(db: Session = Depends(get_db)): + """Get list of unique sizes from products""" + products = db.query(Product.sizes).filter(Product.sizes.isnot(None)).all() + sizes_set = set() + for product in products: + if product.sizes: + for size in product.sizes: + sizes_set.add(size) + return sorted(list(sizes_set)) diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index 8a8cd64..6f678b2 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -3,33 +3,30 @@ from sqlalchemy.orm import Session from app.database.database import get_db from app.models import User from app.schemas.user import UserResponse, UserUpdate -from app.services.auth import verify_token +from app.services.auth import get_current_user router = APIRouter(prefix="/api/users", tags=["users"]) -def get_current_user(token: str, db: Session = Depends(get_db)) -> User: - user_id = verify_token(token) - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", - ) - user = db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - - @router.get("/me", response_model=UserResponse) -def get_current_user_profile(token: str, db: Session = Depends(get_db)): - user = get_current_user(token, db) - return user +def get_current_user_profile( + current_user: User = Depends(get_current_user), +): + return current_user @router.put("/me", response_model=UserResponse) -def update_user_profile(token: str, user_update: UserUpdate, db: Session = Depends(get_db)): - user = get_current_user(token, db) +def update_user_profile( + user_update: UserUpdate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + for field, value in user_update.dict(exclude_unset=True).items(): + setattr(current_user, field, value) + + db.commit() + db.refresh(current_user) + return current_user for field, value in user_update.dict(exclude_unset=True).items(): setattr(user, field, value) diff --git a/backend/app/routers/wishlist.py b/backend/app/routers/wishlist.py index 7d18cb5..a4fcd6f 100644 --- a/backend/app/routers/wishlist.py +++ b/backend/app/routers/wishlist.py @@ -2,28 +2,20 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List from app.database.database import get_db -from app.models import Wishlist, Product +from app.models import Wishlist, Product, User from app.schemas.product import ProductResponse -from app.services.auth import verify_token +from app.services.auth import get_current_user router = APIRouter(prefix="/api/wishlist", tags=["wishlist"]) -def get_user_id_from_token(token: str) -> int: - user_id = verify_token(token) - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", - ) - return user_id - - @router.get("", response_model=List[ProductResponse]) -def get_wishlist(token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) +def get_wishlist( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): wishlist_items = ( - db.query(Wishlist).filter(Wishlist.user_id == user_id).all() + db.query(Wishlist).filter(Wishlist.user_id == current_user.id).all() ) products = [ db.query(Product).filter(Product.id == item.product_id).first() @@ -33,12 +25,14 @@ def get_wishlist(token: str, db: Session = Depends(get_db)): @router.post("/{product_id}") -def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - +def add_to_wishlist( + product_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): existing = ( db.query(Wishlist) - .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id) + .filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id) .first() ) if existing: @@ -47,19 +41,21 @@ def add_to_wishlist(product_id: int, token: str, db: Session = Depends(get_db)): detail="Product already in wishlist", ) - wishlist_item = Wishlist(user_id=user_id, product_id=product_id) + wishlist_item = Wishlist(user_id=current_user.id, product_id=product_id) db.add(wishlist_item) db.commit() return {"message": "Product added to wishlist"} @router.delete("/{product_id}") -def remove_from_wishlist(product_id: int, token: str, db: Session = Depends(get_db)): - user_id = get_user_id_from_token(token) - +def remove_from_wishlist( + product_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): wishlist_item = ( db.query(Wishlist) - .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id) + .filter(Wishlist.user_id == current_user.id, Wishlist.product_id == product_id) .first() ) if not wishlist_item: diff --git a/backend/app/schemas/__pycache__/__init__.cpython-314.pyc b/backend/app/schemas/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9ee5d23dd3f67d2e08b41c2631447a0a70d3bc3 GIT binary patch literal 181 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08CE6CX@CbT%U zs5mC2EVC#l#y>CBr6{v3HO3{iIJ+djASOMtBsDQ6sVFfoMK?FGxFoeGCMhvFJ2fvQ zCb6I(rZ_nx6(}4NAD@|*SrQ+wS5SG2!zMRBr8Fniu80+A1;`P_AjT(VMn=XWW*`dy Db;K|8 literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/brand.cpython-314.pyc b/backend/app/schemas/__pycache__/brand.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd60bf0a201c369481d786d83fd86b70424e9b87 GIT binary patch literal 1660 zcmbtUOK;mo5T4~rl5JT{<452acH}L`M%-g(c4dJwbSTgTI2?XEnEhsUK7KjfoB&+kzB)~s z4B!v?IA89->HmVxKJ0+cHsMF6*fPw*HSn!@@a=83W}7%=Y1Bc~-L{6Ptx*rrS_O49 zT1T`|L0y01Q)o}Vr-W@*Hg1Q7xSPi!jm#0oT7!@NSLlSuY#S8wL0LPtvgaU&NAL~v z**v6m<@lCzAA$0G8#6nYQifSfc_S7Zkd(U*6mJE_hf7 zSp@O3M5e*LOx%&lGZEYo#Z#5{g2#yxVX#XiuXn>j2^s8$(NmGdLD=gBMf5}Y2LKad~5iHe2?G&Byra}5jw+letG>lgep_uDcfgs^LR;iIjMWH zAqBQbeuR-*C$7ANZ}~Bq%VcKAP!=6vj?b07TOFBq zD`PY6LQ2nMk}F7n+ELJ$9K!2`rI#(!pU)8Z|Js(6x3rR&l$~U%ZAto3nwd3XHbdK_ zxP{SbE3)7;MiuSVCtv)awP(ocd2H9m^+Qr2a+Zvq%z5&zP_~L>J|U2dK8E5@GW&4m zF?abdKyy|@o&P=<9SYpS8?N`B$6ZVJz$G=AUp=eu2A0ptynN nPn$9J4wgmoENanK?|} literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/cart.cpython-314.pyc b/backend/app/schemas/__pycache__/cart.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..589f677524beb227c3b635d57e8db51c29cb9962 GIT binary patch literal 2894 zcmbVO-ESL35Z}EoU;GuPX+GLCag*|8+5|)^RA{Iw5=0`kE*xzogxlkCzNAO(JF~Z` z6rP-d$_tg6KO_GK|9~Hm!bw#@@X|LNTZBhu_U!hYI4N``&+g66&F#$m=C?kZ%_Iqo zKfi9fKTCxCfe+oGb{4INu&9$e#GuP$iE^69wX`fP$y{DixB}1AkeAgZjca)_LZ*qK zj1fbwA+MJgK+7N#^q7s zLpWS9C|BX9-6H3yA&rrW&J%{r^;?7w7!;18gxLj)@n!B*7noylr)X2~hd&$&{4n~~ zHz4L|o!laKq>DcgQU>iQ@;SXr-yvVnugE3Ipl^UpXDxNs6rQDXCt;&Az)n75Pr*Gn zmii3C3zM}d0j66H^>5cLkGp&$R08)qCsb{};P7| zSF9i?yEZq?06oss!{Wk+rJI4nf>Qau%POVop0mi@`%Y=m3GQ*fR$6npW0h8z<&|fv zR=^!rTCwbVj#n;OwOT2#?>e9^*^sum+D4c*P0RCq4!JYUaH2PxbMZ|L4iLFf-e=BmpAUU5Cg^G%bb;cb>dkp&SB#m=|7?%CjA z1X2aTxVC9=&fJxC?gXF)%je0ool3^{fM+F!XBF@a_y%0>!}HBr`EWeH56+Lm zGvJo)!>zzKU{hkqHX@U-M&PkQ5U?J?wa7_?wJ0gaLHhz1(W!cHf#nF$D#Ar}bSrpN zZ74Il2#zRAW3XG$**Hw>1PW{^h6#=;dJ0#QD5g;4LB#7ajY<^aQ+P~Kmz}AZ`qCbL zrCSZO>8S>Whof}`L{Bw_qG|}5qd;@gg$^Tisv`@wQ4OV29Tifig}H8j^yM^!5{?#z z7h|*&T*j&!60n&pSZi#*pScC!TXolNFicf(TkDsraB!9R)fld$l;Imuni0gI7I|WC z!UN{Il>qUO0DgZOjLhB!MSq%n06Ry)>JXYl`g|!ZA|(6JsoJ4a8GL9R`b-FjhLT%m zDLB#ou1|DmwV(*(Mi|baA@=RjK%P91_!xzl2(91-hzQDhYV*^G;sn`s7;f;`=Cv-l zb=hvLiKNg>K4-h}W(pN)lAUZQr*<mt~^ z0RQSXUB=g=)SV04WN&c@)yr}mXF)aRoc|1!G-Cdiw?We|VKl+q_; u>M5CiLSAcY3O)V!%Qk_jou%{)u3>8H12q45qfKCHk8800;BNwxi2Pr)!$0f* literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/category.cpython-314.pyc b/backend/app/schemas/__pycache__/category.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b57743d4788539ccc135b4f6b650e1523ac89119 GIT binary patch literal 2335 zcmai$&u`pB6vt=$Yn|O>w@FFUCQUYJX~hbfN;HRxmZ~B_O$4OE&`5}FENAU(O}+Lq zGl`Vj77$#h)E+qSC-fiTo?Ea~A;hV-ZZ{$vd2ei|-t7`b+UMstzur85=9|1*9xD=P zi@zQEw-iGD!k4V+snctN(;=J0qHAQ83R*>9SyNV3p{{B|lke)9zN!noN+!tyv9u{- z>G$c9nyeuV88skU$VCe>YC?1*7d5ToMN%uhz^OGFk9_2C@3YADf|?SWciX~`LMMo| zPkk<++jJe_3BTnvlC2U8XOXS91P=$YzfT0Uh)^~Sp`IZToJDBx(>KU9YAI7BFoj{M zqOd`PX;Ii;i_6Qfax-Fk%gh6By+L6u{9)7JhllHZ1Lh{}kPWh_T>pU(7oz9M=kx)+ zO1_|9lN*XfFTYxxR^M_O553T>JMDIzHy(Hpt~b(hEw=aKQQLOH zFcJ{5?RciYl8gCH1Cro3!2I(Q`HuWv9DB6ZRSBhkN>2x`YmkZQUoZYLyRTj97BFCv z(YKy1KmKrEtG*h6Ko6#qXQVa~o3@RWfjx`1-HP0u0DZ{j+Z`uJdn&fQ?K3U{KlH-L zw%HiWW@Rw(crLT;ov;COC!tVq48LJJLNI@8M|d2)Diu`c@lNJWeoL)mCzi2hMFS5Wfh2me+oNg zX%k?R+Mb5!Yp{}Y^?`-UalHO1SQAgLJ-)K9%_Ve=pUa3!5qnJ(B9?rQGcdbWWK-~9 z`1RO1Gh+LqH+#NLFH~eV7L? zfccR?*?z7h!`^|wa9!Soo|9nC=`saBgBATuhm|4VN*#vyus%K2XhT9BE54gxG772W z=vHIZ4~3*V73h-?pM9geIgPUM7VtY@63ETcqdRY^XQ>RDnhIy8q;p*qo^J72NzcA2 z^>M#mGlzXRh86!m+~3AAXk>c$Q6=t2WvW-Z1k6z-M>-B%N9}I!xlRa5%kU3tY>2(K zA2t&y!2d-Us%WCA^XSQ428c0lCZd0fU4(lyMTNAds}sD~H9k?GOCV zB^T0 zq$$}N-YXhG5N27fkdz=OHcldIYkmlcL%xII?+5TL{8Dd@cPAz=_WRvA@t-(_Ds6o#-U+Gm@r7qDQs7u?kl!Lmgc{XapDOQhD?QuEG zGkIM1Glfpi;+}J=Usk5aw9+e;ub;fy7N+~j!40v33+#0rv5H@@M&UGtGZ>0iU*v;1 z2E|L%MKd%NL|5VHmBa=%h#*OfeGfmMefh_h9=3^zlYQ00p8w%1^4QE*giBvl-m@#e z8JKq}ztzM7Zd4daEa>MZ7AZ6+G$~(?O6$jX(ym#UNv6aSb<+KJ4bdW8ibR`DtK#=V)nx Nbsm246!24?^B?{Hb_oCg literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/model.cpython-314.pyc b/backend/app/schemas/__pycache__/model.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eee4462c58b02395461515a5ede7f0135d0d86bf GIT binary patch literal 2849 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 Ng(nyPA@C8A`5%yPN~r(< literal 0 HcmV?d00001 diff --git a/backend/app/schemas/brand.py b/backend/app/schemas/brand.py new file mode 100644 index 0000000..134e7b3 --- /dev/null +++ b/backend/app/schemas/brand.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel + +class BrandBase(BaseModel): + name: str + +class BrandCreate(BrandBase): + pass + +class BrandUpdate(BrandBase): + pass + +class BrandResponse(BrandBase): + id: int + + class Config: + from_attributes = True diff --git a/backend/app/schemas/category.py b/backend/app/schemas/category.py index a354ebb..99b2c56 100644 --- a/backend/app/schemas/category.py +++ b/backend/app/schemas/category.py @@ -7,11 +7,13 @@ class CategoryCreate(BaseModel): name: str slug: str description: Optional[str] = None + image: Optional[str] = None class CategoryUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None + image: Optional[str] = None class CategoryResponse(BaseModel): @@ -19,6 +21,7 @@ class CategoryResponse(BaseModel): name: str slug: str description: Optional[str] + image: Optional[str] class Config: from_attributes = True diff --git a/backend/app/schemas/model.py b/backend/app/schemas/model.py new file mode 100644 index 0000000..722f59d --- /dev/null +++ b/backend/app/schemas/model.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from decimal import Decimal + + +class ModelCreate(BaseModel): + name: str + category_id: int + brand: str + base_price: Optional[Decimal] = None + sizes: Optional[List[str]] = [] + description: Optional[str] = None + + +class ModelUpdate(BaseModel): + name: Optional[str] = None + category_id: Optional[int] = None + brand: Optional[str] = None + base_price: Optional[Decimal] = None + sizes: Optional[List[str]] = None + description: Optional[str] = None + + +class ModelResponse(BaseModel): + id: int + name: str + category_id: int + brand: str + base_price: Optional[Decimal] + sizes: Optional[List[str]] + description: Optional[str] + created_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index 7b65d74..6015b63 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -1,30 +1,37 @@ from pydantic import BaseModel from typing import Optional, List from datetime import datetime +from decimal import Decimal class ProductCreate(BaseModel): name: str + slug: Optional[str] = None description: str price: float discount_price: Optional[float] = None category_id: int + model_id: Optional[int] = None gender: str # men, women brand: str sizes: List[str] - colors: List[str] + colors: Optional[List[str]] = [] stock: int images: List[str] is_featured: bool = False is_on_sale: bool = False + override_price: Optional[Decimal] = None + override_sizes: Optional[List[str]] = None class ProductUpdate(BaseModel): name: Optional[str] = None + slug: Optional[str] = None description: Optional[str] = None price: Optional[float] = None discount_price: Optional[float] = None category_id: Optional[int] = None + model_id: Optional[int] = None gender: Optional[str] = None brand: Optional[str] = None sizes: Optional[List[str]] = None @@ -33,23 +40,29 @@ class ProductUpdate(BaseModel): images: Optional[List[str]] = None is_featured: Optional[bool] = None is_on_sale: Optional[bool] = None + override_price: Optional[Decimal] = None + override_sizes: Optional[List[str]] = None class ProductResponse(BaseModel): id: int name: str + slug: Optional[str] description: str price: float discount_price: Optional[float] category_id: int + model_id: Optional[int] gender: str brand: str sizes: List[str] - colors: List[str] + colors: Optional[List[str]] stock: int images: List[str] is_featured: bool is_on_sale: bool + override_price: Optional[Decimal] + override_sizes: Optional[List[str]] created_at: datetime class Config: diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 16c8a33..1ddc7f5 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -29,6 +29,7 @@ class UserResponse(UserBase): postal_code: Optional[str] country: Optional[str] is_active: bool + is_admin: bool created_at: datetime class Config: diff --git a/backend/app/services/__pycache__/__init__.cpython-314.pyc b/backend/app/services/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07afa10651f92e20190d938be6f7f588260cba73 GIT binary patch literal 182 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08CE7;j8CbT%U zs5mC2EVC#l#y>CBr6{v3HO3{iIJ+djASOMtBsDQ6sVFfoMK?FGxFoeGCMhvFJ2fvQ zCb6I(2B@_xGdZ<5CO$qhFS8^*Uaz3?7Kcr4eoARhs$CH)&<>C*ib0G|%#4hTMa)1J E0JAqRbN~PV literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/auth.cpython-314.pyc b/backend/app/services/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bfafeb9484447c372edda9ad6825baf3e1fa78e GIT binary patch literal 5836 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 literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/cart.cpython-314.pyc b/backend/app/services/__pycache__/cart.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..038f8c2903dfdefb6b49613d6b4a864df0531ee4 GIT binary patch literal 5281 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 XK4-jo7n%pSS0cKOZ$vmEY{-8Dk-k^r literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/product.cpython-314.pyc b/backend/app/services/__pycache__/product.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..885da87dfaec286d1a55fe4559ac079d7205a2b4 GIT binary patch literal 5134 zcmbtYO>7&-6`tiTcS$aPtsl#-=|3fMX}gl+*pcDNNo!d)CA$i}w&_D@sT8@A8B-)P zyRxIA0capaC8UPZLPZ?JKvbYWW57Ut@S%queDEPhGn6A|;X(!)wD66BW9ZuVX1Pm> zVrwNCfHQCA&Ay#^@0)Mla-ZMlAyB^g)wh|9i;#a|$0~ePc>YHaie!W++#rc^I=2I5 z$Dkw1>wHwu1=i;Wol#L2qmnMMzA)&Dx^;KdqkDD`Ws@RqCW^F~WP@2wmlYTE+}P8- ziU(ww$v(vkvX9Ar#Se0T$pNKB2|_P~J$OE-I6FzC^%*WO!Wq&zRns#0T*P5WF`7?L zrF6s9pVZZfJe`I?Uv;pbsxWAGFBHRNyFGSa+bTyDO>_HW>Fjohb1QjOWMEHk_b`wx*4K1zGj9{Hv?i~mNhe+ zFP$OQ-mXY>vMa&jjW!eBs%cVhyT=8dbw9&h7>@LqA*ZT-9L=N+aYD_dRcc82+^Ck! zs)lPEJUT_yv>|9$G6ln#%}i!=L!>H<<{}bB&rvrT<1;zk;5D5Z!dO0^rMNSSE`k7x z<9Rxn)HU?^ZbRumnz*2;R7<3Fpd|70?$ z=_*Z(B~w>mZHZ)|kbp<8W>Tt_D41C2E=(KV(a~fsm)Bu4qoZ^SyhnncmW8TFR!J-O z+6q=rT!9J??Khztk6t~f#dRwTZ*`ExgZQIW9cBL4RI^o@~<8+#rS*gUV_$W>(Q zd5P?=_)zi_Z=e!DDM&=m@Av%n(n<(=&*6O}g|D^>dVs;}3)nouD`o-DngMMs+=r^# zf)wZWRA;0+5sGms=B#_9D%PHi*9igQPzgw)w|d`Fu$JnL*a8f^0F#@1&8v(6UZ zU`yDbEgRZ(wuA;-QY=u@T2kC;c_W4gzoEoMTiep0g6^T}P$#*~4M#kN^W7W9kkWVBK17|9^pCrQkYk87$`X3Ty9ZW_$0w2hyY7sHrsfY+oku zFs_^Jd4^fqC{z4UR*2yi8%ho7bQ`E5PKG|_2a20D9B^4U$1_yZBMuY1T`;WScDlMu zV20{|IXX6t-rWmo=BsWHisZ5En>#XlWd2lH-ew}>FQ@)G^yi_wdrPnMmE~hga(_wg zFUtcDg@N@D!Qe7?^)X}L48} zSQvhbVWwC{YGTV-$1)h1jZqCAA+3k)VU>6gWl$l?pt;k}ng;+j=q;Z6#ev{c~Zyp=Faz_Z(kLOMy@(F~6Yr#O16xNU|-3u!@e67IBX z@CG`Nun~Fj5M0dF)$Jgsfrj0{6oA<#n6xZ0%;L#yC~>|H^?1Jd;q#D#`qcMQ*{QTT zI+cUlHzG1NqUcJ)3Hj!vW=Jre)OAWTHdppQKlqBh^uy{Dt7$e$> zRXYxF|NBzx1-;FO5*J({P$5#GL8L-+?*toM&xc4a_@?I+sv>EMO-I~LX6vv?nqhMd zi#27+8m1HuS?=U{*eeFj5YuW_h0G2yVX+#610q4gMb_xdoN5RD=b&U>Ktt;S-}=<# zubFoig)KIheJuyplNW-Vx#JyX==)uDKwjupRutz;i3~r$#X2Ix^s_D-Wri>UEHDeTGymWesIxHul@hSo?g7;kQLYsUf@R$Bw zd>*>_o3eNRooj!4|MvS!J;zHu$M2uLKXrfNi?jFp%0011-q>^A;qk1B#1out|48_7 z+p^m~+cqs zqdda{e>`+g=HY~EW*Us?meGQv8ct1W-B#v^oJ%o&hNw>$GPw!!uMoV+*!P)v@u2r9 zUJVpo!F)n7EoLh;9p~zU7fxl>H|a1a0NX@63{{2aIPRZh_cHnMGU;0;yH-W`=ix;+ aet5iaLALoK2-p9Mtw6A=t=9QsX8#M3ra+$n literal 0 HcmV?d00001 diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index a5db368..74743e5 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -2,11 +2,15 @@ from datetime import datetime, timedelta from typing import Optional from passlib.context import CryptContext from jose import JWTError, jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.config import settings from app.models import User from sqlalchemy.orm import Session +from app.database.database import get_db pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBearer() def verify_password(plain_password: str, hashed_password: str) -> bool: @@ -37,11 +41,17 @@ def verify_token(token: str) -> Optional[int]: payload = jwt.decode( token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm] ) - user_id: int = payload.get("sub") - if user_id is None: + user_id_str: str = payload.get("sub") + if user_id_str is None: return None + # Convert string to integer + user_id = int(user_id_str) return user_id - except JWTError: + except JWTError as e: + return None + except (ValueError, TypeError) as e: + return None + except Exception as e: return None @@ -50,3 +60,32 @@ def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: if not user or not verify_password(password, user.hashed_password): return None return user + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db), +) -> User: + token = credentials.credentials + user_id = verify_token(token) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + ) + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + ) + return user + + +def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User: + if not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Admin access required", + ) + return current_user diff --git a/backend/app/utils.py b/backend/app/utils.py new file mode 100644 index 0000000..c8f0f41 --- /dev/null +++ b/backend/app/utils.py @@ -0,0 +1,32 @@ +import re +import os +import uuid +from pathlib import Path + +def generate_slug(text: str) -> str: + """Generate a URL-friendly slug from text""" + # Convert to lowercase + slug = text.lower() + # Replace spaces and special characters with hyphens + slug = re.sub(r'[^\w\s-]', '', slug) + slug = re.sub(r'[\s_-]+', '-', slug) + slug = re.sub(r'^-+|-+$', '', slug) + return slug + +def save_upload_file(upload_file, folder: str = "products") -> str: + """Save uploaded file and return the file path""" + # Create uploads directory if it doesn't exist + upload_dir = Path("uploads") / folder + upload_dir.mkdir(parents=True, exist_ok=True) + + # Generate unique filename + file_extension = os.path.splitext(upload_file.filename)[1] + unique_filename = f"{uuid.uuid4()}{file_extension}" + file_path = upload_dir / unique_filename + + # Save file + with open(file_path, "wb") as buffer: + buffer.write(upload_file.file.read()) + + # Return relative path for URL + return f"/uploads/{folder}/{unique_filename}" diff --git a/backend/migrations/001_add_model_table.sql b/backend/migrations/001_add_model_table.sql new file mode 100644 index 0000000..a026d85 --- /dev/null +++ b/backend/migrations/001_add_model_table.sql @@ -0,0 +1,24 @@ +-- Migration: Add model table and update product table +-- Create model table +CREATE TABLE IF NOT EXISTS model ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + category_id INTEGER NOT NULL, + brand VARCHAR(100) NOT NULL, + base_price DECIMAL(10, 2), + sizes JSONB, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + UNIQUE (name, category_id, brand) +); + +CREATE INDEX IF NOT EXISTS idx_model_category ON model(category_id); +CREATE INDEX IF NOT EXISTS idx_model_brand ON model(brand); + +-- Add new columns to product table +ALTER TABLE product ADD COLUMN IF NOT EXISTS model_id INTEGER REFERENCES model(id) ON DELETE SET NULL; +ALTER TABLE product ADD COLUMN IF NOT EXISTS override_price DECIMAL(10, 2); +ALTER TABLE product ADD COLUMN IF NOT EXISTS override_sizes JSONB; + +CREATE INDEX IF NOT EXISTS idx_product_model ON product(model_id); diff --git a/backend/migrations/002_add_brands_table.sql b/backend/migrations/002_add_brands_table.sql new file mode 100644 index 0000000..191e105 --- /dev/null +++ b/backend/migrations/002_add_brands_table.sql @@ -0,0 +1,24 @@ +-- Migration: Add brands table +-- Date: 2026-05-01 +-- Description: Create brands table for storing brand names + +CREATE TABLE IF NOT EXISTS brand ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE +); + +CREATE INDEX IF NOT EXISTS idx_brand_name ON brand(name); + +-- Insert existing brands from products +INSERT INTO brand (name) +SELECT DISTINCT brand +FROM product +WHERE brand IS NOT NULL AND brand != '' +ON CONFLICT (name) DO NOTHING; + +-- Insert existing brands from model table +INSERT INTO brand (name) +SELECT DISTINCT brand +FROM model +WHERE brand IS NOT NULL AND brand != '' +ON CONFLICT (name) DO NOTHING; diff --git a/backend/requirements.txt b/backend/requirements.txt index 669064b..6a093da 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,11 +1,11 @@ -fastapi==0.104.1 -uvicorn==0.24.0 -sqlalchemy==2.0.23 -psycopg2-binary==2.9.9 -python-dotenv==1.0.0 -pydantic==2.5.0 -pydantic-settings==2.1.0 -python-multipart==0.0.6 -python-jose[cryptography]==3.3.0 -passlib[bcrypt]==1.7.4 -email-validator==2.1.0 +fastapi>=0.104.1 +uvicorn>=0.24.0 +sqlalchemy>=2.0.23 +psycopg2-binary>=2.9.11 +python-dotenv>=1.0.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +python-multipart>=0.0.6 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +email-validator>=2.1.0 diff --git a/backend/schema.sql b/backend/schema.sql index 8079bfb..26976d6 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -1,6 +1,48 @@ -- E-Commerce Database Schema --- Run this file to create tables and populate initial data --- psql -U ecommerce_user -d ecommerce_db -h localhost -f schema.sql +-- Run this file to create database, user, tables and populate initial data +-- Usage: psql -U postgres -h localhost -f schema.sql + +-- ============================================ +-- CREATE DATABASE AND USER +-- ============================================ + +-- Drop existing database and user if they exist (for clean setup) +DROP DATABASE IF EXISTS ecommerce_db; +DROP USER IF EXISTS ecommerce_user; + +-- Create database (Windows compatible - uses system defaults) +CREATE DATABASE ecommerce_db + WITH + OWNER = postgres + ENCODING = 'UTF8' + TABLESPACE = pg_default + CONNECTION LIMIT = -1; + +-- Create user with password +CREATE USER ecommerce_user WITH PASSWORD 'password'; + +-- Grant all privileges on database to user +GRANT ALL PRIVILEGES ON DATABASE ecommerce_db TO ecommerce_user; + +-- Connect to the ecommerce_db database +\c ecommerce_db + +-- Grant schema privileges including CREATE permission +GRANT ALL ON SCHEMA public TO ecommerce_user; +GRANT CREATE ON SCHEMA public TO ecommerce_user; + +-- Make ecommerce_user owner of public schema to avoid permission issues +ALTER SCHEMA public OWNER TO ecommerce_user; + +-- Grant privileges on all current and future tables +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user; +GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user; + +-- Grant privileges on future objects +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user; -- ============================================ -- CREATE TABLES @@ -11,9 +53,27 @@ CREATE TABLE category ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, - description TEXT + description TEXT, + image VARCHAR(500) ); +-- Model Table (e.g., New Balance 9060) +CREATE TABLE model ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + category_id INTEGER NOT NULL, + brand VARCHAR(100) NOT NULL, + base_price DECIMAL(10, 2), + sizes JSONB, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + UNIQUE (name, category_id, brand) +); + +CREATE INDEX idx_model_category ON model(category_id); +CREATE INDEX idx_model_brand ON model(brand); + -- User Table CREATE TABLE "user" ( id SERIAL PRIMARY KEY, @@ -26,6 +86,7 @@ CREATE TABLE "user" ( postal_code VARCHAR(20), country VARCHAR(100), is_active BOOLEAN DEFAULT TRUE, + is_admin BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -33,10 +94,12 @@ CREATE TABLE "user" ( CREATE TABLE product ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE, description TEXT, price DECIMAL(10, 2) NOT NULL, discount_price DECIMAL(10, 2), category_id INTEGER NOT NULL, + model_id INTEGER, gender VARCHAR(50), brand VARCHAR(100), sizes JSONB, @@ -45,10 +108,16 @@ CREATE TABLE product ( images JSONB, is_featured BOOLEAN DEFAULT FALSE, is_on_sale BOOLEAN DEFAULT FALSE, + override_price DECIMAL(10, 2), + override_sizes JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE SET NULL ); +CREATE INDEX idx_product_slug ON product(slug); +CREATE INDEX idx_product_model ON product(model_id); + -- Cart Table CREATE TABLE cart ( id SERIAL PRIMARY KEY, @@ -284,61 +353,88 @@ INSERT INTO product (name, description, price, discount_price, category_id, gend ); -- Insert Sample Users (for testing) -INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active) VALUES +INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active, is_admin) VALUES + ( + 'admin@example.com', + '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123 + 'Admin User', + '1234567890', + '123 Admin Street', + 'New York', + '10001', + 'USA', + TRUE, + TRUE + ), ( 'user@example.com', - '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123 + '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123 'John Doe', '1234567890', '123 Main Street', 'New York', '10001', 'USA', - TRUE + TRUE, + FALSE ), ( 'jane@example.com', - '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123 + '$2b$12$jNWZdZNMbKEkOjA8Gq3ZUOTml23zfqhFDPJ8AlZQ51WhUyi04AH7C', -- password: password123 'Jane Smith', '9876543210', '456 Oak Avenue', 'Los Angeles', '90001', 'USA', - TRUE + TRUE, + FALSE ); -- Create carts for users INSERT INTO cart (user_id) VALUES (1), - (2); + (2), + (3); -- ============================================ --- SET PERMISSIONS +-- SET PERMISSIONS (After all tables are created) -- ============================================ +-- Grant all privileges on all tables, sequences, and functions to the user GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ecommerce_user; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ecommerce_user; GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ecommerce_user; +-- Ensure future objects also get permissions +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ecommerce_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ecommerce_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ecommerce_user; + -- ============================================ -- COMPLETE -- ============================================ -- Schema created successfully! -- --- User: ecommerce_user -- Database: ecommerce_db +-- User: ecommerce_user +-- Password: password -- Host: localhost:5432 -- +-- Connection string for .env file: +-- DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db +-- -- Tables created: 9 -- - category, user, product, cart, cart_item -- - order, order_item, user_wishlist, contact_message -- -- Demo accounts: --- - user@example.com / password123 --- - jane@example.com / password123 +-- - ADMIN: admin@example.com / password123 (is_admin: true) +-- - USER: user@example.com / password123 +-- - USER: jane@example.com / password123 -- --- Sample data: 5 categories, 9 products, 2 users +-- Sample data: 5 categories, 9 products, 3 users -- -- Next steps: --- 1. Update backend/.env with your credentials --- 2. Run: uvicorn app.main:app --reload --port 8000 +-- 1. Update backend/.env with: DATABASE_URL=postgresql://ecommerce_user:password@localhost:5432/ecommerce_db +-- 2. Run: python backend/app/main.py +-- 3. Login as admin to create/edit/delete products diff --git a/ecommerce.db b/ecommerce.db new file mode 100644 index 0000000000000000000000000000000000000000..8521be72b45ed35c69a29b7149f63716bc7dee7c GIT binary patch literal 122880 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 G0{;R3CcQ}j literal 0 HcmV?d00001 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index febeb1b..9430c8a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -19,6 +19,8 @@ import Wishlist from './pages/Wishlist' import About from './pages/About' import Contact from './pages/Contact' import Sales from './pages/Sales' +import Admin from './pages/Admin' +import Models from './pages/Models' function App() { return ( @@ -42,6 +44,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 3ba4d48..bdae2db 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -6,12 +6,11 @@ const api = axios.create({ baseURL: API_URL, }) -// Add token to requests +// Add token to requests as Bearer token in Authorization header api.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { - config.params = config.params || {} - config.params.token = token + config.headers.Authorization = `Bearer ${token}` } return config }) diff --git a/frontend/src/components/CategoryCard.jsx b/frontend/src/components/CategoryCard.jsx index 24165d9..06c8ba8 100644 --- a/frontend/src/components/CategoryCard.jsx +++ b/frontend/src/components/CategoryCard.jsx @@ -2,7 +2,7 @@ import React from 'react' import { Link } from 'react-router-dom' export default function CategoryCard({ category }) { - const categoryImage = `https://via.placeholder.com/300x300?text=${category.name}` + const categoryImage = category.image || `https://via.placeholder.com/300x300?text=${category.name}` return ( diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 8fe840d..cf91dc3 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -32,6 +32,9 @@ export default function Navbar() {
  • Sales
  • About
  • Contact
  • + {user?.is_admin && ( +
  • Admin
  • + )}
    diff --git a/frontend/src/components/ProductFilters.jsx b/frontend/src/components/ProductFilters.jsx index 71139a3..8ef933e 100644 --- a/frontend/src/components/ProductFilters.jsx +++ b/frontend/src/components/ProductFilters.jsx @@ -1,12 +1,62 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' +import api from '../api' import '../styles/global.css' export default function ProductFilters({ onFilter }) { const [filters, setFilters] = useState({ gender: '', - priceRange: 'all', + brand: '', + model_id: '', + size: '', + min_price: '', + max_price: '', onSale: false, }) + + const [brands, setBrands] = useState([]) + const [models, setModels] = useState([]) + const [sizes, setSizes] = useState([]) + + useEffect(() => { + fetchBrands() + fetchSizes() + }, []) + + useEffect(() => { + if (filters.brand) { + fetchModels(filters.brand) + } else { + setModels([]) + setFilters(prev => ({ ...prev, model_id: '' })) + } + }, [filters.brand]) + + const fetchBrands = async () => { + try { + const response = await api.get('/products/filters/brands') + setBrands(response.data) + } catch (error) { + console.error('Error fetching brands:', error) + } + } + + const fetchModels = async (brand) => { + try { + const response = await api.get(`/models?brand=${brand}`) + setModels(response.data) + } catch (error) { + console.error('Error fetching models:', error) + } + } + + const fetchSizes = async () => { + try { + const response = await api.get('/products/filters/sizes') + setSizes(response.data) + } catch (error) { + console.error('Error fetching sizes:', error) + } + } const handleFilterChange = (key, value) => { const newFilters = { ...filters, [key]: value } @@ -14,6 +64,20 @@ export default function ProductFilters({ onFilter }) { onFilter(newFilters) } + const resetFilters = () => { + const resetFilters = { + gender: '', + brand: '', + model_id: '', + size: '', + min_price: '', + max_price: '', + onSale: false, + } + setFilters(resetFilters) + onFilter(resetFilters) + } + return (

    Filters

    @@ -31,19 +95,73 @@ export default function ProductFilters({ onFilter }) {
    - +
    + {filters.brand && models.length > 0 && ( +
    + + +
    + )} + +
    + + +
    + +
    + +
    + handleFilterChange('min_price', e.target.value)} + style={{ width: '80px', padding: '0.5rem' }} + /> + - + handleFilterChange('max_price', e.target.value)} + style={{ width: '80px', padding: '0.5rem' }} + /> +
    +
    +
    -
    diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 49f4ba6..563bd7e 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,20 +1,37 @@ import React, { createContext, useState, useEffect } from 'react' +import api from '../api' export const AuthContext = createContext() export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null) const [token, setToken] = useState(localStorage.getItem('token')) - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(true) useEffect(() => { if (token) { localStorage.setItem('token', token) + fetchUserData() } else { localStorage.removeItem('token') + setLoading(false) } }, [token]) + const fetchUserData = async () => { + try { + const response = await api.get('/users/me') + setUser(response.data) + } catch (error) { + console.error('Error fetching user data:', error) + // If token is invalid, clear it + setToken(null) + setUser(null) + } finally { + setLoading(false) + } + } + const logout = () => { setUser(null) setToken(null) @@ -23,7 +40,7 @@ export const AuthProvider = ({ children }) => { return ( - {children} + {!loading && children} ) } diff --git a/frontend/src/context/CartContext.jsx b/frontend/src/context/CartContext.jsx index 1a3aebb..97c672d 100644 --- a/frontend/src/context/CartContext.jsx +++ b/frontend/src/context/CartContext.jsx @@ -18,12 +18,11 @@ export const CartProvider = ({ children }) => { setTotal(newTotal) } - const addToCart = (product, quantity = 1, size = null, color = null) => { + const addToCart = (product, quantity = 1, size = null) => { const existingItem = cart.find( (item) => item.product.id === product.id && - item.size === size && - item.color === color + item.size === size ) if (existingItem) { @@ -35,7 +34,7 @@ export const CartProvider = ({ children }) => { ) ) } else { - setCart([...cart, { product, quantity, size, color }]) + setCart([...cart, { product, quantity, size }]) } } diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx new file mode 100644 index 0000000..970d141 --- /dev/null +++ b/frontend/src/pages/Admin.jsx @@ -0,0 +1,1224 @@ +import React, { useState, useEffect, useContext } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Admin() { + const navigate = useNavigate() + const { user, token } = useContext(AuthContext) + const [products, setProducts] = useState([]) + const [categories, setCategories] = useState([]) + const [loading, setLoading] = useState(true) + const [showForm, setShowForm] = useState(false) + const [editingProduct, setEditingProduct] = useState(null) + const [formData, setFormData] = useState({ + name: '', + slug: '', + description: '', + price: '', + discount_price: '', + category_id: '', + model_id: '', + gender: 'men', + brand: '', + sizes: '', + stock: '', + images: '', + is_featured: false, + is_on_sale: false, + override_price: '', + override_sizes: '', + }) + const [uploadingImage, setUploadingImage] = useState(false) + const [uploadedImages, setUploadedImages] = useState([]) + const [models, setModels] = useState([]) + const [activeTab, setActiveTab] = useState('products') // products or categories + const [showCategoryForm, setShowCategoryForm] = useState(false) + const [editingCategory, setEditingCategory] = useState(null) + const [categoryFormData, setCategoryFormData] = useState({ + name: '', + slug: '', + description: '', + image: '', + }) + const [searchQuery, setSearchQuery] = useState('') + const [filterBrand, setFilterBrand] = useState('') + const [filterCategory, setFilterCategory] = useState('') + const [filterModel, setFilterModel] = useState('') + const [brands, setBrands] = useState([]) + const [allProducts, setAllProducts] = useState([]) // Store all products for filtering + const [showBrandForm, setShowBrandForm] = useState(false) + const [editingBrand, setEditingBrand] = useState(null) + const [brandFormData, setBrandFormData] = useState({ name: '' }) + const [brandsList, setBrandsList] = useState([]) // Separate list for brand management + + // Redirect if not admin + useEffect(() => { + if (!user?.is_admin) { + navigate('/') + } + }, [user, navigate]) + + useEffect(() => { + fetchProducts() + fetchCategories() + fetchModels() + fetchBrands() + }, []) + + const fetchProducts = async () => { + try { + const response = await api.get('/products') + setAllProducts(response.data) + setProducts(response.data) + } catch (error) { + console.error('Error fetching products:', error) + } finally { + setLoading(false) + } + } + + const fetchCategories = async () => { + try { + const response = await api.get('/categories') + setCategories(response.data) + } catch (error) { + console.error('Error fetching categories:', error) + } + } + + const fetchModels = async () => { + try { + const response = await api.get('/models') + setModels(response.data) + } catch (error) { + console.error('Error fetching models:', error) + } + } + + const fetchBrands = async () => { + try { + const response = await api.get('/brands') + setBrands(response.data.map(b => b.name).sort()) + setBrandsList(response.data) + } catch (error) { + console.error('Error fetching brands:', error) + } + } + + // Filter products based on search and filters + useEffect(() => { + let filtered = [...allProducts] + + // Search filter + if (searchQuery) { + filtered = filtered.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) || + p.brand?.toLowerCase().includes(searchQuery.toLowerCase()) || + p.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ) + } + + // Brand filter + if (filterBrand) { + filtered = filtered.filter(p => p.brand === filterBrand) + } + + // Category filter + if (filterCategory) { + filtered = filtered.filter(p => p.category_id === parseInt(filterCategory)) + } + + // Model filter + if (filterModel) { + filtered = filtered.filter(p => p.model_id === parseInt(filterModel)) + } + + setProducts(filtered) + }, [searchQuery, filterBrand, filterCategory, filterModel, allProducts]) + + const handleChange = (e) => { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value + setFormData({ + ...formData, + [e.target.name]: value, + }) + } + + const handleImageUpload = async (e) => { + const files = e.target.files + if (!files || files.length === 0) return + + setUploadingImage(true) + const newImages = [...uploadedImages] + + try { + for (let i = 0; i < files.length; i++) { + const file = files[i] + const formDataUpload = new FormData() + formDataUpload.append('file', file) + + const response = await api.post('/products/upload-image', formDataUpload, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + + // Add the full URL + const imageUrl = `http://localhost:8000${response.data.url}` + newImages.push(imageUrl) + } + + setUploadedImages(newImages) + // Update form data with new images + const currentImages = formData.images ? formData.images.split(',').map(s => s.trim()).filter(s => s) : [] + const allImages = [...currentImages, ...newImages.map(url => url)] + setFormData({ ...formData, images: allImages.join(', ') }) + + alert('Images uploaded successfully!') + } catch (error) { + console.error('Error uploading image:', error) + alert('Error uploading image: ' + (error.response?.data?.detail || 'Unknown error')) + } finally { + setUploadingImage(false) + } + } + + const removeImage = (imageUrl) => { + const currentImages = formData.images.split(',').map(s => s.trim()).filter(s => s) + const filtered = currentImages.filter(img => img !== imageUrl) + setFormData({ ...formData, images: filtered.join(', ') }) + setUploadedImages(uploadedImages.filter(img => img !== imageUrl)) + } + + const handleSubmit = async (e) => { + e.preventDefault() + + const productData = { + ...formData, + price: parseFloat(formData.price), + discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null, + category_id: parseInt(formData.category_id), + model_id: formData.model_id ? parseInt(formData.model_id) : null, + stock: parseInt(formData.stock), + sizes: formData.sizes ? formData.sizes.split(',').map(s => s.trim()) : null, + images: formData.images.split(',').map(i => i.trim()).filter(i => i), + override_price: formData.override_price ? parseFloat(formData.override_price) : null, + override_sizes: formData.override_sizes ? formData.override_sizes.split(',').map(s => s.trim()) : null, + } + + try { + if (editingProduct) { + await api.put(`/products/${editingProduct.id}`, productData) + alert('Product updated successfully!') + } else { + await api.post('/products', productData) + alert('Product created successfully!') + } + + setShowForm(false) + setEditingProduct(null) + resetForm() + fetchProducts() + } catch (error) { + console.error('Error saving product:', error) + alert('Error saving product: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const handleEdit = (product) => { + console.log('Edit clicked for product:', product) + try { + setEditingProduct(product) + const imageList = Array.isArray(product.images) ? product.images : [] + console.log('Setting form data...') + setFormData({ + name: product.name || '', + slug: product.slug || '', + description: product.description || '', + price: product.price || '', + discount_price: product.discount_price || '', + category_id: product.category_id || '', + model_id: product.model_id || '', + gender: product.gender || 'men', + brand: product.brand || '', + sizes: Array.isArray(product.sizes) ? product.sizes.join(', ') : '', + stock: product.stock || '', + images: imageList.join(', '), + is_featured: product.is_featured || false, + is_on_sale: product.is_on_sale || false, + override_price: product.override_price || '', + override_sizes: Array.isArray(product.override_sizes) ? product.override_sizes.join(', ') : '', + }) + setUploadedImages(imageList) + console.log('Showing form...') + setShowForm(true) + console.log('Form should now be visible') + + // Scroll to top to show the form + setTimeout(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + }, 100) + } catch (error) { + console.error('Error in handleEdit:', error) + alert('Error loading product: ' + error.message) + } + } + + const handleDelete = async (id) => { + if (!confirm('Are you sure you want to delete this product?')) return + + try { + await api.delete(`/products/${id}`) + alert('Product deleted successfully!') + fetchProducts() + } catch (error) { + console.error('Error deleting product:', error) + alert('Error deleting product') + } + } + + const resetForm = () => { + setFormData({ + name: '', + slug: '', + description: '', + price: '', + discount_price: '', + category_id: '', + model_id: '', + gender: 'men', + brand: '', + sizes: '', + stock: '', + images: '', + is_featured: false, + is_on_sale: false, + override_price: '', + override_sizes: '', + }) + setUploadedImages([]) + } + + const handleCancel = () => { + setShowForm(false) + setEditingProduct(null) + resetForm() + } + + // Category Management Functions + const handleCategoryChange = (e) => { + const { name, value } = e.target + setCategoryFormData({ ...categoryFormData, [name]: value }) + } + + const handleCategoryImageUpload = async (e) => { + const file = e.target.files[0] + if (!file) return + + setUploadingImage(true) + const formData = new FormData() + formData.append('file', file) + + try { + const response = await api.post('/products/upload-image', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + // Prepend backend URL for image display + const imageUrl = response.data.url.startsWith('http') + ? response.data.url + : `http://localhost:8000${response.data.url}` + setCategoryFormData({ ...categoryFormData, image: imageUrl }) + alert('Image uploaded successfully!') + } catch (error) { + console.error('Error uploading image:', error) + alert('Error uploading image') + } finally { + setUploadingImage(false) + } + } + + const handleCategorySubmit = async (e) => { + e.preventDefault() + + try { + if (editingCategory) { + await api.put(`/categories/${editingCategory.id}`, categoryFormData) + alert('Category updated successfully!') + } else { + await api.post('/categories', categoryFormData) + alert('Category created successfully!') + } + setShowCategoryForm(false) + setEditingCategory(null) + resetCategoryForm() + fetchCategories() + } catch (error) { + console.error('Error saving category:', error) + alert('Error saving category: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const handleEditCategory = (category) => { + setEditingCategory(category) + setCategoryFormData({ + name: category.name || '', + slug: category.slug || '', + description: category.description || '', + image: category.image || '', + }) + setShowCategoryForm(true) + setTimeout(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + }, 100) + } + + const handleDeleteCategory = async (id) => { + if (!confirm('Are you sure you want to delete this category?')) return + + try { + await api.delete(`/categories/${id}`) + alert('Category deleted successfully!') + fetchCategories() + } catch (error) { + console.error('Error deleting category:', error) + alert('Error deleting category: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const resetCategoryForm = () => { + setCategoryFormData({ + name: '', + slug: '', + description: '', + image: '', + }) + } + + // Brand management handlers + const handleAddBrand = async () => { + const newBrand = brandFormData.name.trim() + + if (!newBrand) { + alert('Please enter a brand name') + return + } + + try { + await api.post('/brands', { name: newBrand }) + setShowBrandForm(false) + setBrandFormData({ name: '' }) + fetchBrands() + alert('Brand added successfully!') + } catch (error) { + console.error('Error adding brand:', error) + alert('Error adding brand: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const handleUpdateBrand = async () => { + const oldBrand = editingBrand + const newBrand = brandFormData.name.trim() + + if (!newBrand) { + alert('Please enter a brand name') + return + } + + if (oldBrand === newBrand) { + setShowBrandForm(false) + setEditingBrand(null) + return + } + + if (!confirm(`Update brand from "${oldBrand}" to "${newBrand}"?`)) { + return + } + + try { + // Find the brand ID + const brandToUpdate = brandsList.find(b => b.name === oldBrand) + if (!brandToUpdate) { + alert('Brand not found') + return + } + + // Update the brand + await api.put(`/brands/${brandToUpdate.id}`, { name: newBrand }) + + // Update all products with this brand + const productsToUpdate = allProducts.filter(p => p.brand === oldBrand) + for (const product of productsToUpdate) { + await api.put(`/products/${product.id}`, { ...product, brand: newBrand }) + } + + // Update all models with this brand + const modelsToUpdate = models.filter(m => m.brand === oldBrand) + for (const model of modelsToUpdate) { + await api.put(`/models/${model.id}`, { ...model, brand: newBrand }) + } + + setShowBrandForm(false) + setEditingBrand(null) + setBrandFormData({ name: '' }) + + // Refresh data + fetchBrands() + fetchProducts() + fetchModels() + + alert('Brand updated successfully!') + } catch (error) { + console.error('Error updating brand:', error) + alert('Error updating brand: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + const handleDeleteBrand = async (brand) => { + const productCount = allProducts.filter(p => p.brand === brand).length + const modelCount = models.filter(m => m.brand === brand).length + + if (productCount > 0 || modelCount > 0) { + alert(`Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).`) + return + } + + if (!confirm(`Are you sure you want to delete the brand "${brand}"?`)) { + return + } + + try { + // Find the brand ID + const brandToDelete = brandsList.find(b => b.name === brand) + if (!brandToDelete) { + alert('Brand not found') + return + } + + await api.delete(`/brands/${brandToDelete.id}`) + fetchBrands() + alert('Brand deleted successfully!') + } catch (error) { + console.error('Error deleting brand:', error) + alert('Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error')) + } + } + + if (!user?.is_admin) { + return null + } + + return ( +
    +

    Admin Dashboard

    + + {/* Tab Navigation */} +
    + + + + + Models + +
    + + {/* Products Section */} + {activeTab === 'products' && ( + <> +
    +

    Manage Products

    + +
    + + {/* Search and Filters */} +
    +
    + + setSearchQuery(e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + border: '1px solid #ddd', + borderRadius: '4px' + }} + /> +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + {showForm && ( +
    +

    {editingProduct ? 'Edit Product' : 'Create New Product'}

    +
    +
    +
    + + +
    + +
    + + +
    + +
    + + + {(formData.brand || formData.name) && !formData.slug && ( + + Will generate: {(formData.brand + ' ' + formData.name).toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-')} + + )} +
    + +
    + +