z?@X!)_Gi+>TFc#UCJCt!1mA)9cs%4fhOpsi8chZaHPwNVO!B3p#9#{p>!#)nAR z0hH&NZKCCfmkU@FZv?K)y%yL7Snw+R%_NW`m_>(c*Xq8l{$_Ndn%Wx}|19uD@UOu~ z%3MR9bKr_he%RRwth;LmyN9;dxo5K7C+{FxfbEVTDRlB*?m|+k5QwrilAem^Wl^Tf z70+|;2oJ~EyU6gQ&-!Kno&=(BEC@G)+zpP|I;1^fV*B4OYXvK3Z9qt3DzY#VJP7+G zkffJkxH*oPMD8$T&%@Bfn}xf;^?~AEzi0r%3O$ai-U3o3Kl(}JY&&S9iF&M_-g&JV zeY2X{SNh$sP3?x?ZusBc8#uf3##d8wkKm`wH{^NllOth^!1J*ENkSijeKc%IJ7r%E zv+l{^Me72}@Lu9s%mLdx!BKQdik`rl;smst;t^bp@ByBO$aO$Fmg0(AnmAgQLFzU5 zn-_r`304JzHMpx>I0jNM-WZr{C>I;@MF*e%(}njx;d2(O?ZStb=>#vZw3E_NT*Fb3 zj$M2vy0DYM5vMToGJ)B~@Lh(%V`A(b#U%bKrIIoIf^ZmS*&%R5Ijfo;Nm=C7yI8?9+cy(winn^nh^W$6;=S&pP z%n5EJJe~=|7c*0EK`&oG*?KNZ-4%GHIk!JgFCHX_$+5Ls*?N~(ZFpTcZz;*Nm@VZB z7EexFf|9@sfi0Jcre(`|E^F~y&X!7LizggD0P+;dF~$=I@KeH};8o?B|5s2S%X#`1 zTY!0dp_m^6ZApS4d_%4@$(66k=-1@u-;mTJlKQvk6~uoLBw-?qe;bI^#L;TAPeb>H?q91_szXiT{Qmi=o%!t>Pl!i|*7{o5)yJM< zcOVh*LQ4d$6-BAxd&Bpa>$BD2rjXp9`|UrL|DJh5Bw=2t%|OYAzW$NZfA;!QK1Kz< zXo)!03cC9?f+JfxZanpE>Sp72zyI>o6Uiqm2z#-3%VAEQcA4=vT;|vFPx%z;n{J7? z+&Ra{y^%&F(G*VaUzz>e^5 KH61bN_Uk>g?LOvW14Q(lX=$n%CDNp^+N^8lk z>x16QbwL05&)08efB%me{W}zDMNm!+eq+8AMCfbY@C&rP^0 ~JD%YY7b_0TFBj zjNk%ZYq=xTwoJ5>_Qk>a{!Nw9N&+PO6!fgA{#;lukd`}eMqcKz(MIHX!3e!0->G$> zM8}Vyg-Li19F25(Sd!7+gw+LD2kXF16`X7@tbHU@$LeUp>H(}HO(1pE`0wZHB$|h( z7+p cWLr zPA{cM)-^NfIA(#@h~v~$>8$Cv(4{O8ch;B#EjX^bWFbTIfEM=kaY~? tJ)DgYaU6}AWY)0)`lJzn3APr-dtjmt1mbV!FFL kgq#(zs$$D|s$WCa8IZBqxY-+0A5=3#Lo- iyh$CGayPgwQl5l7FCsYc)ApvzOQx`&(R3r&;ua0nTZqXndw7NZXkEdvkh!=Pv>Jh zicjZ1@SiYHig)w#g5XC1=B5wqfPkit;*IZOv~397Fv84j6qjo(ONN ryY*r W%YE6?V dQsak2u Gwh(I2}c&PJy{-l%;fa2R2+yV37!1<^kT%n-q0lo=X@LHaZo{U9ztt`Cu= zl$A5cY!0ptH?B59m&W+K0WO~5f`=`I%z~H )kE4q~<4mUN#IFwqLYc+Rhc-|;X;5#~h22n)SR@C0{lVknM$k0aQP&snw z-qBLzP$lx)jmTszTUPXujD=tYVQYEpS{`rpWskcgzl<> eX*n-D@n(^tTq#mvcNpNG~kIv`ROQk1}|1v*m=v+t<)uA=SMQsrrhzF;^wnv zdV%sAfOW&8WCY%5`kd~NErs&`4Xl-qdG9Z$mgBp4hr{7?XT;B?l>hsnDi>{hl~Q}+ zFcI!f?;*oPGwphpCzG-nxP5tEXJR(zQXXe?j0;X29pb{f6#4D@BzOWg&spRoJq=BM z4LYYmLU>^O1sW}*(Ff?62dMV}8h(KGe}P^tqgTIb=_vIbynEvA$a>4mMFk8X1mEpj z?pryv>Xr^1E$<&K_EqqUn*+nQdj7uuA;S2;`@e@c#0MUWNH|s%K&uApi%M-zmk$4i zFF3jR+}ORIPhf$ByH- EW2PmF&$`Ok9? zk%D_y(^X#G9eTtozU=v`09v) ){t}vh z4XQYw>avKPh2t6_z6M5G8N|C2=IL zpwwbyH9V+S&Qzo;e~VpOX0dC7y+iJ#j|tPdv4@L@ved1Sk-;0&IqdnCTTDZy4P#16 zP%U786lJ-wqRTD%X3p>DKC^(+601FI=_1(lF{?jp9m3Reixkf$TTRbu&6SccEd)G` z>Jgl`rkUcK1 4WGg%(i|L*90}mtIjP)K2zZ2cJffT>@W+n(4`q;sL0TGk2htG|<)KVLR?cU# zi4aqBuH#rf(|8Lw>9Mp-s(1*g7IATu9SxaN!KA6F>EK>hhjE``LJc%!5!<+1#NEW2 z!bUfy6Ta&i1 &F4PonR4xvX#MJks;Iwx~FFY~$}3%V$a9Np(LE?tr( zCJPz2?vXuA7BgPmC;OP}%J}tw9MFSukiAQpkRF!9dW+nmN92g!Dz~zIccx8mm)kkS zBh0BD)jP|r_?CG)7P&+9<4!ezyV9Z>d?3o*xMTjf6Yw@yLpZn+cC5e?kA2H<3%b%A z^U;C>Y6MGZNo{@LTDDb4?g4)%8!tPU+J-$ixDR&z$1C+Hs_i>G?thENj{l6X6L+yF z?FhSe_je>!Ry=z!)g}e%m8i$$@1QsyS(#52iyM?oh{y` =VrMOhV6#B7sYVT1X+)p`JX<$_JVItuS3*v$?{X!s95v}FwoGcn);yqn z?^3xH-oeb5V0j(Ig|WrpmF$9&(bO1= tM==P0ToZoC@Dyzno#oj1YsaB5_T2`@ v^ U*P{=ED6THC4J zMTwNJk^|sjF)&E0-RP%OV_<>;p=P5Tg_pOapWl&wuGDM;NFO%_prxoGAs}(VzI5}x z1}#(bn%k(e;V=htCP#|o2pl>Lk3nbXuFLv+Dj!yE*ZhZ9#l!YR{DWWE#+rK9@;Kry zKs9glFF _>-XNb`&b7@+*R43qyedN*vR^gJH86 z9;f3lMuC;i6|$<0Akj`%5L_@oFc<+=;&g)nrkOkP9_$%=km7AlSJDUSetP6IkX=~| zL@O7TJheca0py#}N7uf+wsiBy_74CnRbh2#^hN06s^{XGzoh|>vG@VYorb{yGndek z5qkd}5DyujXOv884(r8%9MK6)fhC#6yq29Mw9%{%7&HU4zh@K!HwEIQx(L-7)LJpX z9MVGQ9}D8A%2uYtWI;<|N6Q+F<%pt+gtwhI=ahmu$A&5lfBPp+Fic_zTGnJ4w#0l6 zHfS4J0_2QgjHT=`VnAYz_r`Mu=2Ibq&k>qL`Ws>V4Wuq`9QO*Ht)a6o(V>^9>m@q! z3Z43kcX9j{0`g}7aeY5~TPyw5JJtCY-qU5NF1fk3rQSLMR_)!Qtlo}z&sWj=(T9^u z!E&_54Xj@tU!Az|EWC+4T% y!JV^3#a)3+@F@q=|9q SLZ>j`yAVA)qZMrYW?K7rwdQ+Z9?>s ZicqI)3BC`kdh<19)cW)$+%UrO^AGEJU{U}8 literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/products.cpython-312.pyc b/backend/app/routers/__pycache__/products.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91633dcea0af69f57a55c73c99ace43197c31eb2 GIT binary patch literal 3364 zcmai0O>7&-6`uXklH#vON}@$cCMCzRSTs`O#!?*HF={)A9VC(=B`vTZB2b)>!g!aY zXP1uU(uiCXsC{$M0^w0d8PY+gUUEs1OMqMy$OmeWz$w}ue52$527Ky!yGx38Y;}ow z^JeDFoAtQpZN1U-{M9m(($BgMQGp@&- zIiHa*d-YzkPwzAP^?oy{C(R@J5pzHvkcdo}ME$gTS=tP2fK~YEDVF+d+T}Yb4KhEA z4Zzf9s3l9o-jC1-=#p7*Lvr^l`Dm1NvmunHJ-`h+TnxCQ4i|582{uG~q4hCsDMI`I z+_m8ytEXRKN%AF@{L%u29$~|@Q@fuIbZF1KLVJqE4m^CpD~S$zma259L)QrCde<|% zqXWzi(>2U`9IsV+v_sc1&~>FliO16aZYd9eo &xw_A zcJwhS%BrIKcUP~>m8&-6!gq;PSczI9eEFk~uD*M-z$$jJToRgP=k2PMm4t7eSr$xX zWubmhv~1y@bwu(8ENaWlUZ9Jx=DW(vv|51Wi+P(Zm-$Lo5n*rgB4;ouqTX tpiib%mx)shjBs WS?t8lhdyeU*WzibHt?nSs~>4I4X#WRn40YDKfcpjobfIx{Nkmb^X zl{c91FM+RB&ZtnWYsHG_GKyx=7BXEd_&Nd!1%@clo{RzclGxtWBc!I)NKLBAHKnH3 zx;CW^Z`VA@$}?Gw4}m-%MsgHM8c7Dp2#~DAu~KDyhl}x15VBBl#{HhnU3@$DvBkKR zqc@7&$jz46C0@M2a+jEO%`R7R%SD^zbBjD*qOY5I3vws7m@ixd=X3c=CC8m~vT`jr zCn_r |(ZxFnv79 !yjn$rW?_|Mo+R48)$@MjY#6>s6V7NiN8yG z0+QsGZ_*8-kggY0obf&ldI!P`Fwa4#xDWz(p1cd5{2Wkyuq&(b6VM&ZyU5@!1ctu` z4BQ1Lhj
YpG>=E(zf=JDSld!EIvy7wkiD%+W* za o^BAb(T|lx5{}u9>5W?4r9v;|sKiF{#`EOz64QLi3 z0~kcoYx3IrJCVuN_jZHvA0mV6%J!S@J&au5_FrxU !+bv=qIF_Mnb9jXnlDnIJT{hIVOonD==;a zDYQX`Er^U0oOghE&N>-0Yr~Vht<;o5!=ua-ju`{SW6rpUt1jYGIDxP`ep#3g0J)cP zNw2K2n{b7SrRBC~Z-Ll(Xx2Co@GL;0!}Ynf)cPkok+X-!;N|cimVJl0MpfEXlpsLb z41XO=;S)$Efe4=o4@Q2O-5P2oq+63|#;O(yu(nh+j1_ V8Ve2|1zXKa&RHU9++ zXJG65&{{Bf0sib^F#AFdhWfUXr?!Jr+v=1P_w2avEkXT)SAb&}IRfuk!Lgo*h;`k_ z8--7qxiV4aCdVQvG<&52QQ&@*#mX)Epy*A&qm^IGTdd7+`~xWhxaK?<77yZ%5Y8rW z) (Pek|@gbIx>vkC<0DxA*|@`jm3-LW|D&)RxqKtrB^6$O@&e{0kLc@ z)WveyaB8kox15VB)JhrF@OB8LQnfi|6IKO$GC5-UfVlVdHQ=B5&w>+GgT2E)fFV9m z)>EKOMUtdP PLb2YUio%(6ycw;EjI6B%$A8Yi)8nMJ~e4sw|x5>R|Ow!<8 zs2}_5a8m*Pr%> wtHM?;XAi+p8h+K$-qRlXJ z5z^hWj~bsSM4oEOI5*xh=*k*hKmYyM88ql@Bb{02|4e?{|4n+2NYdMP3r+0m38!0k zyRFanaT1lCYs$cZ37&Ve>$8rY-N~t~8yjCBDvA2VCU$Fo*vHP>1ss?Ky9VzJ-kn>E zt`6= FeKrwXjEI>BL&NiQU%pK6be4jiwA7IQYU@|GIGH+p%vZ_Y|L$X!InS zPJjHQ+rM>wAIILQz%_eu|L~pRyI<@`>D@CIzMlH(JlZ+EuArUMTS>HY8h2#&Ahgp? GV*C%Ejvm|q literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/users.cpython-312.pyc b/backend/app/routers/__pycache__/users.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54e1699746961a74ba4fc620758c35f0eaeedfe8 GIT binary patch literal 2831 zcmbtWO>7fK6rTO@+VNi!2*gcj8=$2zrAbNyL5QCc96_K8C?rByuH21x95(FQW_BGB z>J(EU5!6cMfJBZ|s?^YMK 2% z5>@zGGTf#o>fvkIs4~4#FJHS1pXrbK%|JB3-xVWhR!6G^A`&WSZp||&JgAx!OEIH0 zuq#r}Bt*uM6f`fb)qGUZ{M4INw7?BlbQ7%^`k-{aT53V+e^6cV^g2IDS*y9>nk*Ye z>%iBCtG$%g|G`%+4dA`tPMcQw+xnWjZGPk3>S#UpC2Lz&`3j9}4cEWG3x#E;>e$)t z-gMTc%yCC(hNd*j37qWfJKJ?7K{K|VPC1HYt9I533y%97wJg|zr7}qExHbSQx! yToTeF~GIW9hdxsqWtsb6tq zTOEXLuIY%{fTJ*KXIUyyhLR^z1}VX_xCG)ZvCFU$k|Py}N5!0&Z?sX14;yXx7y@Fh zADZw$tU&y=kQ0Wg)@u-Ab86#$CB{(TO D&QahGj}SUz|x14N$81slfq z6@uIICuZHj{I$7Y<6P*SnNa(5sC{ X& z>4&->>%Xs`I5EBblY*~(#&>Yqcd+0)l#e|1RfC@iX{tT)B-l0O>6-Ob=T9#}JaScP zHIeB-R<3}!3HdiRX6-v*u$Hwsp~61oEg&oOga!VS!qQ55*}Gt9A*CUwtO;f(v_22< z3`+w>l)VqfA$Y9KAoAo-U*mY!y}bqBu6%@Fo9#e*ALcHe<%OD9E*3aJLhus%6&C9* zyId+jQekMd73kvN `M(Q6YJRe7=d07?k#?JnFLD6}6m{lXI@gvS)#OTPKs8 zxKUA7JPCM?@UI&=sZy+ ?}&KT~vt6eY5*|_l?uDvU2m(^;2W^6S-~$Z|J>Wa>R9Qlws(N3HlS9+$DhRa$|*! z^01d1aV4`GA-tlsQM=8NHEnQ%W?R7(%K_TpGP0VWi4UYou|TWFVv&1TJ=~5V-B}$7 zfT#i2=$JL4*oNY56i7!XD*W*)=&dcPAQlx*)4&KcGqx9Wcf(_Kg2 9X7BVC{lToZR sYzYL2v|naQv?oGs7{Ti~Or4yYDgUNHP?<#_Ro zcO9$Lt7~w>LJz9d!h0ls!5HS!spV&s4eJSNwWwM9BL5h`*S7R$K(g#oOLZhj25%;g zl*!r*IqOJz%H~ 7SW_m{3Im#zuZJ%|YtT6HXlu;OX?7XmVK>i_@% literal 0 HcmV?d00001 diff --git a/backend/app/routers/__pycache__/wishlist.cpython-312.pyc b/backend/app/routers/__pycache__/wishlist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e001ce03f9d16f9e0d5a3cc39861e0bc5a1bd000 GIT binary patch literal 3689 zcmdT`T}&I<6~6Q1@!0qeYyy}iYhbJ5MUZSD*;Q4N4QrCHX(0=tsVrCS;JJ`-W^C`B zfeq9MQ$<>$t+ZMx)#i~(D>YG&mdBO$B~@Q4^##jRkWo-lwSB1iV#2CY_o?TOJ+@)f z)jqTjz0%xs?mhP$U!U`xbM7C#UN?d<+WR- S-7Ys~HXAI|XM%GR5mFw59jonV+oS|ehrl1>= zv7kp8(>aZG9kzhwf28OJP+=Y$NqIKPn&J&bzp28m>AXg=@ $z-3$FSvD z#6+1}N*dW)IJ4|MCFPV;AK0p*&4EE7NAR3-+vE*tUN;@qc~c mz?E`B%H 48tDC@r9;zVQ9CC0}>p z@}ARISbgg2eA<3;w|%(SKD-y~`O3lh-DSkN++`7Yf`y4gP(mhxi26u1x=9T@p_;5b zo2sT6J%gO^S@HR~mGUo*51VJwwdhz{)>My^9mzDcc5Kv7rS0<4a3as%>O(aoq?;N< zRn|_~6)w-k(8qj=okuD5XOjr6@c-dn)Nj`K)_c`XrG57KI?eOxKz(I%NA4Q5ck9%? z>nH55-s91V18(S;hTAgmt?(Z}JSL)UE3~HL{RK>x2(87YFsG=171B-@oTRFR`oUxs z*>vYB=?W+`9dinSycR5Ql9O wid9Wrr$~=jgV0)OdMM;<9TJ7D z6Y%Muz*Ing^}V(+Q}Xo}E 1 zv?z|2#8{a_j$ql1T6_QL-~z&PPXKmYO_q9FN$l7aBSkUtS=(>JzX?AuK3{#fTI@bw z5-;qEV?}Z7PvYe=2Wo{Y0O5c@Asi}9{_BMc7CJ@L?}8UYqy0S8qhHkBu|i%0QK}J_ zG_PNdWyVGVposmSVVR@&Xu!MGXyHx`BabW$RWmS6%h9o6V+gRrlTO2Li;Wr{(=GO1 zogS@<2K9tB@?0Kma(2C3alpTCwPkghci8@65S(bMOKi@*6*O;Nb||#!3KPqn)eaJo zRDvaWDWYT|HDHB67b+ R5=J9ezO-+_DE!j1tsK?3<5}``eeWaUCH5=r6 zbXl+8p=$j;ogBRwOHPfwH#0VU9a2b^O(B)lG=-)M#mVuU5%F#4hDv7M^x7A$WY6(r z@q;ZW$@P4p^|%*4{ns!x=1W `864!m6)c>D3Y z#es>EYx0@PcenMUwz3mBya#S12JE6fRBRvG3!bhQ^{*hax;Acn5#9;K3P0ZCg%2;S zU0NU6;X5clzBYd6>Yk@{yY0+&)6h0QL^|PSFHeluj~Ds>)5Ord^~8`FnXN+{o&;=~ zcJE^&&mV#Kw-oOq5LfPj$Cg9mP*Ee9tnm$ilYaP} DI{&xU1 zeW<7yTEQS#jXGY2Tyh#jI?aHFiXN)`Zsq-8 CJ8aO$(3{(N|3896XBPO-X2#COn<$x$M*5S0y!d z6KhL@S)vhI`AxxC%0ZSA`gAZ`;Sn_`ONKNn>A1!a7nQlG%zEPJmBL}Q&>-E_sZB{A zto9B<8^76XNibPdQdl397L1$L+YsKcl?OMc8^q+4j8SRV9P~`9RhoP*3nE$t$Pk^V zmmebQFIw&OYw?G0jRh5-C!-(&_#pisFqJumVV Vd;RJC)hb3IvNx}IFP^my^h)dSSb3~zu2kO%Kr zWb4KYx}u6F%Per^KC0NY*0sJ`VqSd`yZVPqzn?rnUgoWh=`tl-L5qCx{TFma6(a{M IaF#Rv4e`AR^8f$< literal 0 HcmV?d00001 diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..8aebd1f --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from datetime import timedelta +from app.database.database import get_db +from app.models import User +from app.schemas.user import UserCreate, UserResponse +from app.services.auth import ( + authenticate_user, + create_access_token, + get_password_hash, + verify_token, +) +from app.config import settings + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + + +@router.post("/register", response_model=UserResponse) +def register(user: UserCreate, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.email == user.email).first() + if db_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered", + ) + + hashed_password = get_password_hash(user.password) + db_user = User( + email=user.email, + full_name=user.full_name, + hashed_password=hashed_password, + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +@router.post("/login") +def login(email: str, password: str, db: Session = Depends(get_db)): + user = authenticate_user(db, email, password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + ) + + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + access_token = create_access_token( + data={"sub": user.id}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "user": UserResponse.from_orm(user), + } + + +@router.post("/verify-token") +def verify_token_endpoint(token: str): + user_id = verify_token(token) + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) + return {"user_id": user_id, "valid": True} diff --git a/backend/app/routers/cart.py b/backend/app/routers/cart.py new file mode 100644 index 0000000..4ab6d38 --- /dev/null +++ b/backend/app/routers/cart.py @@ -0,0 +1,67 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.database.database import get_db +from app.schemas.cart import CartItemCreate, CartItemUpdate, CartResponse +from app.services.cart import ( + add_to_cart, + get_cart, + update_cart_item, + remove_from_cart, + clear_cart, +) +from app.services.auth import verify_token + +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) + 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) + 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) +): + 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") + return {"message": "Item updated", "quantity": cart_item.quantity} + + +@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) + 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): + 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 new file mode 100644 index 0000000..898f384 --- /dev/null +++ b/backend/app/routers/categories.py @@ -0,0 +1,60 @@ +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.schemas.category import CategoryCreate, CategoryResponse, CategoryUpdate + +router = APIRouter(prefix="/api/categories", tags=["categories"]) + + +@router.get("", response_model=List[CategoryResponse]) +def get_categories(db: Session = Depends(get_db)): + return db.query(Category).all() + + +@router.get("/{category_id}", response_model=CategoryResponse) +def get_category(category_id: int, db: Session = Depends(get_db)): + category = db.query(Category).filter(Category.id == category_id).first() + if not category: + raise HTTPException(status_code=404, detail="Category not found") + return category + + +@router.post("", response_model=CategoryResponse) +def create_category(category: CategoryCreate, db: Session = Depends(get_db)): + # TODO: Add admin check + db_category = Category(**category.dict()) + db.add(db_category) + db.commit() + db.refresh(db_category) + return db_category + + +@router.put("/{category_id}", response_model=CategoryResponse) +def update_category( + category_id: int, category_update: CategoryUpdate, db: Session = Depends(get_db) +): + # 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") + + for field, value in category_update.dict(exclude_unset=True).items(): + setattr(category, field, value) + + db.commit() + db.refresh(category) + return category + + +@router.delete("/{category_id}") +def delete_category(category_id: int, db: Session = Depends(get_db)): + # 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") + + db.delete(category) + db.commit() + return {"message": "Category deleted successfully"} diff --git a/backend/app/routers/contact.py b/backend/app/routers/contact.py new file mode 100644 index 0000000..2084216 --- /dev/null +++ b/backend/app/routers/contact.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from app.database.database import get_db +from app.models import ContactMessage +from app.schemas.contact import ContactMessageCreate, ContactMessageResponse + +router = APIRouter(prefix="/api/contact", tags=["contact"]) + + +@router.post("", response_model=ContactMessageResponse) +def send_contact_message(message: ContactMessageCreate, db: Session = Depends(get_db)): + db_message = ContactMessage(**message.dict()) + db.add(db_message) + db.commit() + db.refresh(db_message) + return db_message diff --git a/backend/app/routers/orders.py b/backend/app/routers/orders.py new file mode 100644 index 0000000..a3de381 --- /dev/null +++ b/backend/app/routers/orders.py @@ -0,0 +1,51 @@ +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.schemas.order import OrderCreate, OrderResponse +from app.services.order import ( + create_order, + get_order_by_id, + get_user_orders, + update_order_status, +) +from app.services.auth import verify_token + +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) + if not order: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot create order with empty cart", + ) + 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("/{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) + order = get_order_by_id(db, order_id) + if not order or order.user_id != 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 new file mode 100644 index 0000000..1915c1b --- /dev/null +++ b/backend/app/routers/products.py @@ -0,0 +1,79 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Optional +from app.database.database import get_db +from app.models import Product, Category +from app.schemas.product import ( + ProductCreate, + ProductResponse, + ProductUpdate, +) +from app.services.product import ( + get_products, + get_product_by_id, + create_product, + update_product, + delete_product, + search_products, +) + +router = APIRouter(prefix="/api/products", tags=["products"]) + + +@router.get("", response_model=List[ProductResponse]) +def list_products( + category_id: Optional[int] = None, + gender: Optional[str] = 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, + ) + + +@router.get("/search", response_model=List[ProductResponse]) +def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): + return search_products(db, q, skip=skip, limit=limit) + + +@router.get("/{product_id}", response_model=ProductResponse) +def get_product(product_id: int, db: Session = Depends(get_db)): + product = get_product_by_id(db, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + +@router.post("", response_model=ProductResponse) +def create_new_product(product: ProductCreate, db: Session = Depends(get_db)): + # TODO: Add admin check + 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) +): + # TODO: Add admin check + product = update_product(db, product_id, product_update) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + +@router.delete("/{product_id}") +def delete_existing_product(product_id: int, db: Session = Depends(get_db)): + # TODO: Add admin check + if not delete_product(db, product_id): + raise HTTPException(status_code=404, detail="Product not found") + return {"message": "Product deleted successfully"} diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py new file mode 100644 index 0000000..8a8cd64 --- /dev/null +++ b/backend/app/routers/users.py @@ -0,0 +1,47 @@ +from fastapi import APIRouter, Depends, HTTPException, status +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 + +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 + + +@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) + + for field, value in user_update.dict(exclude_unset=True).items(): + setattr(user, field, value) + + db.commit() + db.refresh(user) + return user + + +@router.get("/{user_id}", response_model=UserResponse) +def get_user_by_id(user_id: int, db: Session = Depends(get_db)): + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user diff --git a/backend/app/routers/wishlist.py b/backend/app/routers/wishlist.py new file mode 100644 index 0000000..7d18cb5 --- /dev/null +++ b/backend/app/routers/wishlist.py @@ -0,0 +1,70 @@ +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.schemas.product import ProductResponse +from app.services.auth import verify_token + +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) + wishlist_items = ( + db.query(Wishlist).filter(Wishlist.user_id == user_id).all() + ) + products = [ + db.query(Product).filter(Product.id == item.product_id).first() + for item in wishlist_items + ] + return products + + +@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) + + existing = ( + db.query(Wishlist) + .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id) + .first() + ) + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Product already in wishlist", + ) + + wishlist_item = Wishlist(user_id=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) + + wishlist_item = ( + db.query(Wishlist) + .filter(Wishlist.user_id == user_id, Wishlist.product_id == product_id) + .first() + ) + if not wishlist_item: + raise HTTPException(status_code=404, detail="Item not in wishlist") + + db.delete(wishlist_item) + db.commit() + return {"message": "Product removed from wishlist"} diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/__pycache__/__init__.cpython-312.pyc b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a1735c71d02b2fd17b4f6d9d7e0eb1accea91b2 GIT binary patch literal 179 zcmX@j%ge<81i^AoGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UaoJ2`x@7Dvn7h z%Ph)?@y| h)K^ZNllDNDoV^t(alXPE=etlNlHx4PR&b+Nh~Oc zDNfEv1q#Q+$7kkcmc+;F6;%G>u*uC&Da}c>D`Ev&!3e~~AjU^#Mn=XWW*`dyV#_cG literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/cart.cpython-312.pyc b/backend/app/schemas/__pycache__/cart.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7260591a66887a4fc974a96ad9181845dc96fdaf GIT binary patch literal 1857 zcmah~OK;mo5MGi?iju5H;<#}WSBjk+LKM!WD9{#Z4w2imKq41KeJDYIpv8^DMtpQh z2VhSI>;m>pxBP}6f1|fvS||sBK+r?cLvBT-qJU4GSxOcqr04<~&d%=6&V1j@>gQUm zLSWp#`G@zDOvvvznGJQSj88y$PF&(jed0@|B%v<%WnVEBpPCfric9;dubG-eWb%Nx z>Irc*CcRX0A7)8dOJLOtR$W-jV67CaWnry?b){gfq&1`d2HhD_R=)4V>`~~lz9DD& zZj^Xo;Pf;4(2J8dQog706%XCRUh*}IqcDhBFApV`)40gE3-jlM5mR!BDen=cFzQNo z!RpGUDrSn9QBz|jroRg-utS~N$_1N-wv-yBtln{W@@2yO4rflnvPzVPVtX#vU{j`l zf9M2>m!#c{#@-JsQ+r`Q +Pe@PCpuS&OUub+k9 PZZNB%td`cZBbWL)x3LNLYLw& z^|TD-(wtS=tjaSPY7Be2P!E_+R+`q}Lg3gcIL4UlJaEK1l?~ azPgoiKRp9emS$ zL6-o|8jpGC+fI^jZ~ri1@e;b3mLKIUxC*l#mq45lsFAZqj_W2Ex<+})ae*&E-zx0l zc$VY-rCazlw4p$j;}1aqhkEt+L0+2mOUI8!mCJcWUL93eu^NYTeN@{R(v5$pq_+Pe zAo76F&jMZ-M1Kw|bG(YrMuF%#z2XTGBvN?R)C&?zsRqx*3ZgtCkY5MaS?Lfu6`qSs zg{Ji5g&_WqwAG@!3&MCC{4S#<5WEcw*lk=0(GEEi`hPQl{|!0TZzJIruA+Dkelfb8 zkx*+MKO9vXzcg?BTHPGd&1J;#P}ittdX&2OLiV@ K)geu zbp`k0?#0GF*YD;p={^2F97Fu#dm#Q)BuRQrTECIa*W}hjqtcDRw-W-NNnMg|q7I*l XUY6Q}bVA@WS=T`S_-_IqQ6m2Wt>=OO literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/category.cpython-312.pyc b/backend/app/schemas/__pycache__/category.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9807a4a04d45b0ff7384bbb4761886e657ccd8d6 GIT binary patch literal 1345 zcma)6&1=*^6rai0rrU2tYKv_xrD6{4LBWF*58Xl$bt}al41tjDOxMsP8)wo&ZiQB` zxAj#21?ivSQ4b|h1jU0laVtVkzBk#@4;389Z{EE5c=LYmH+kRRUnS5MroV=tDIs5Q zaN5kS(MiDACyX!}k(j!aqOC<*th;(_xP}_*kr|t=Nr^_52s3sGGdVrb%QIX{c`fi( z&Uq`!YlFAvoY&5)PVYC|=+L5PA&~q@i}A?O3VS)t!d4PQh4DC)8BE(O$aogUyio=! zu^W|i?!kDUaN<%%Ty2$bog0kKgI8G^T1 Q>JAbj2xkj5AJGQ> zYnvID;v$EIRc;k1jS=`{1z;Cx z+#4*Zz5$N w>oQm)7l;eeWxp z_*Pz7VM=VaxM=3y{gfrmCtCV6_c+r+C#i^wPz4U-lEG~_9}z(U%1L1D5Mc|4(`C>K z7bMF_sIz}d4oY3uFXc0 -Lva^+6OQjxm?Ysky0(|9RjotM+XXbb$j zC_ agbU##UP9^(bOl*3wKTtxQFcbRhLRG5{oJk5=!7J4#3u z@`FS+!|hD0$>czUYeMboRy*8Fv|wRFvW(wKnbJar4HoZ T9dFkA6gjK8#*Wfw8e_2I-3szAwtQyW; z!*mIBC6r9(%g}l6#KPc&1m$kUujaF(>^~N&4J3)&O1SjEOjZzZCgm7FUX4wEc*OKb z)iA4O4JKjxQ#Gf7-L-uCZ2sSQmO#Cm>^ 1UG z;cOc3ONFbv0*4v+6!LyZ`d1g;)C;p~pxE-gi-GMV(-O;+bp-5^MAHii #Yx)InjG~-c0D| j2+uO6ezZ<0F|rEje`R_|7ST?R26-7muI)Ah4OtzW~XHS$ee-)WJ2Suceow!YN(BPX z?JIwJsz%5kI2b+p&>3{Wc}`s7Dn1F6hN7UK@iT$iPy?-@$+7C|f!@%;*IeDt2DwH~ zAsKRqxY?(~%~9p0ntrcgNYnr{KY`{YS^(6XKnoHr0$Q3t%}&{>yvEO2ipbw~5_&&! zsc&V3u@`eM3LRf)_q>Ed*l- V zlr$8VG%^neRjKAGw*Zx120d48=rl`nYlv#DHpGkxOh>GvT3J!qV=iTPISsZMg(HbV zoSwt>Tw#3Qc0%s)&aTiB?*}UMR^&&_$_vA`LnolNEef_Bz`=d=i?$6b{9#1(LM|${ z?Sx^(9lU>H+pGjTGsr0sYQh;SgZF|Az>>4Q?VHU931vysee5y6xfjwM<~^p(9hyAi zQQSQAICYx)%n99Vfs=5`n)^=c5e?m@6UWV@^^gMGjPOd=;|{|jk|qaEauv*vq+eY) z+Uu9+j_&ts8&7tgUG3gDzSh$=`j_fYzIgUYcl-F$S6ZDdkCsm50nd%kfP;Bk9%M#T znHznShuIuVjI(m40W)*xomb<_3~otCBi|`lrkERNY*Z>D =r!htHr2=KLzI(oj1YOFdit&o e0P^VLr|MO5a?-y!W)#erdg@t^cbjdi`$# zCOsT}af-aL9zTZ2Ie8cBaiR`Ttx}I29kgdkiif(ZCJ((h_QFHkaa~4}M3lxz3nVBO z$HO>ExZ~R`XfEP>Su1LXoOO0BeU?N?7m_&3)O(1bxdgwY1_qc~Uh8RV{e|V8w%o5> z?rE0+d8MtTOyJ}xCdP{R2_pZ)1XjQ7OcP8@Dgrw5SS6_nFe+6*(KE);y!3K_6{Kg- z{IJWJI6m*nW6cz>CySC~dZZVIHx =w2fCjq=v+x6jmh6}=)V MGj9{v;?$YO;$n5&U|K(EncslGM;P2fBgK`f z#+*&~ST(6quHgJE8vO4}^>LAKKEx;*+#vY|%t$!%OFeC=UqaCoD@R}T=T~~#%7vAJ zwNqV1C7G39tDC=>TRm-SR!_Lcu2mF9+;Q;_ro~WDBFj4{fK=o1sPfS35KGlA`?-`i z)}cJ=3{z*ut@PjMGltT{93-EE`Abz4<%DefPBu@- 3XLGUZ>@PQvW%B bO5k-`)0A4*LcDfbE-Ur!-G2zYB;RiU{?_NQ literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/product.cpython-312.pyc b/backend/app/schemas/__pycache__/product.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93dca1f938a70268252a43c9a6cda3757b35d30b GIT binary patch literal 2668 zcmbtVOK;mo5GKVXO}#AnmAHxJM;=0GH0hy-zS0zkorj%7?jt^w5J1r4S|uWh>XHti zo(wnzqMLk*kG&ST_CNH{O9SN~ED-ci^w3*TyC{%TXNIz6EAFKwz~Rg{yR*B)Z)ShW zW>X40H!uF?f2S+TuV@@TVAHez1U}CcPw~{6QdezNMLkhV)HPeHGn #CAa z?kFC6qIf!2pKEal+aS~cbmSO1LZ}I7@)&9ongTS<&7%m(R>sQyj^Qj-CU3eSe-L=Q zW+kM#){Ok1;npO(?}rh%P0x*Z 3ctcja@6y~O0d<490Fvz0u!o% zXgR(ojVf<=Tu6OGxD8M0q5lmJrBMlLfe68jg32Rl_;t6+!`m|Lht4Lfw zAf)CuB7vL}lW4MyAgGBcP`_99K`O65Tl%oP9&! z`Vn`_bfl|wH;lL_Z@865aMZHfY?i~yR~+DSGfv^PW=oh5mxKXISOxK|(o1D`R(fNT zT{hWE=XdV+ii_K&&fUHB-L)=T>^m~KoqN6X=+0^{o$F}r4{&fM- X%rL@IVx~N;v9;>0_V{jtTKb<&{D+e)7C5CVC1-%MM0#P zNA1F)z^#Nof&gNko84Z1dZqJj`)ZfX_7*M!tETrZ?iRZ2a<8zkee3D_om=fsx@=*H zvL{E*%x~X*dbjgM`$3n@_s-AJyruTXT{ed?G0O7Z#O}(A^ehIxP^94xVWBTyWahf8 zI5g*!0RZ9FF6}~uA_iUUn0u4Eqc7Rz_>6eG6U4{t; bZ^p;3!Fw0YFe;|@Kr;|`6p;|@LW5H)!cJVZ#+h=v@>%cCAmH&-5R zuCeC{I*($fTjSP*%=P>B=R9me|Kl>@d;goMGF72#z;oQ_L^h#$(6f(3A7m2>PHQDB z?e&?V-CSD=8k>IgVaZ}c+?Tmc5!4}mB>au7h=)T&l}0>OT!EF|gEt%j@dUZr&c*+F z5rC4F844_@PN <5FyL!U~>seaOzdF1-FLn4$cZ8$SS zt>A44B~W|>Z;07}^fEKv0pcYqCCz5ba~qLg5gH)Uh+0j*QKgGDzEdfeblp*Fp$t;w zldHWpugACe4e<^HLL!AXK>VSps`^Tq{Y6=Lr4$Z~tXk--94PQPn9r)kST7#TkE?SX U|3HDy!J?`r_tySW;6r=;2Tun=YybcN literal 0 HcmV?d00001 diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1588d13c37f1067a946c1a507640bf45dd493a10 GIT binary patch literal 1817 zcmZ`(&uiR96rRzq)&6icwp+5cYa7QHqC#@%C6qLW*uj*NHMDgp%N8N;j2#iJq|Rv5 z>QFGmE%t4DO8$kCKc&a*VHpTLlpcC3^_D 3Y^A6&Tqo{IpL&38R;1Ngqz%A^d7L>2FpMzH$6+a z`;rU1PT3r9Qq{vj6!~$`=WWkXuCGnsS2f@7Cu|ULup7Sr?I4Koknal%e5t1I2XUNa z0Y)X^j43nm3ps~9@}|B0aTo8AUG_8-QFkxqJ0g6_yE|My$&$2t6lOf=9*7`jxBG$2 zxab}Py(c_o-5^c7viBH*lidOMxRd6>1*aMuFk}PZM>4K2KHD2N7f#w@{$l&;+_Yf( z>fGdpXQ~=Tx-B^5TDfT;<#&l-ZO;~Ubc>gXIfN#{HH7O3`qBlo-n#(&Kvn=Cx@+?% zkBT$6eRW_z!A?0aMmr!Ix0itDP9Yj_l@e#7kifYm&YE)75@%1jT8VR}T)o6qkjts( z7HQZ|vE b(n|($CUUPan=SMpP_9zm zPzZrPCzcUb5I#VdN$3sa5HRKPD*#C7`sVScr{BLY&wdzLo8y)Bc;Wz>DR;F**7C=W4yd}ymNYM_|e(zk+nA7*gF1f*m|+~^XkajDxLhz+9>qFi!SMd z^^MQ@5TLBjrKWPUO4HVYCc9W`g-UC!PHC-`DXq0C95iNH3l7RM7gf0>7HMOROP>fQgZ904N~0{%jY9Mo}}Z@!b1Gy)2Jw z3q`Hm7|$;ib+T64ZQ_`@-Wpl0*A}(c{~-WHym$e`3qvDkL7as>VFIJaqYh0uS)PXR zQPBo^hUrnE%Ug$dx$fV%_lk*oUwjCTP#Ae1;BS*s`a8MximbjOD-*|{i>LX7z-v;Y fbYa+<5O_^ohu$16O$fXu3w63ay!$VKmyY#6YdeQr literal 0 HcmV?d00001 diff --git a/backend/app/schemas/cart.py b/backend/app/schemas/cart.py new file mode 100644 index 0000000..e46965f --- /dev/null +++ b/backend/app/schemas/cart.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel +from typing import Optional, List +from .product import ProductResponse + + +class CartItemCreate(BaseModel): + product_id: int + quantity: int = 1 + size: Optional[str] = None + color: Optional[str] = None + + +class CartItemUpdate(BaseModel): + quantity: Optional[int] = None + + +class CartItemResponse(BaseModel): + id: int + product_id: int + quantity: int + size: Optional[str] + color: Optional[str] + product: ProductResponse + + class Config: + from_attributes = True + + +class CartResponse(BaseModel): + id: int + user_id: int + items: List[CartItemResponse] + + class Config: + from_attributes = True diff --git a/backend/app/schemas/category.py b/backend/app/schemas/category.py new file mode 100644 index 0000000..a354ebb --- /dev/null +++ b/backend/app/schemas/category.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + + +class CategoryCreate(BaseModel): + name: str + slug: str + description: Optional[str] = None + + +class CategoryUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + + +class CategoryResponse(BaseModel): + id: int + name: str + slug: str + description: Optional[str] + + class Config: + from_attributes = True diff --git a/backend/app/schemas/contact.py b/backend/app/schemas/contact.py new file mode 100644 index 0000000..61b769b --- /dev/null +++ b/backend/app/schemas/contact.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, EmailStr +from datetime import datetime + + +class ContactMessageCreate(BaseModel): + name: str + email: EmailStr + subject: str + message: str + + +class ContactMessageResponse(BaseModel): + id: int + name: str + email: str + subject: str + message: str + created_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/schemas/order.py b/backend/app/schemas/order.py new file mode 100644 index 0000000..ab78b30 --- /dev/null +++ b/backend/app/schemas/order.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +from .product import ProductResponse + + +class OrderItemCreate(BaseModel): + product_id: int + quantity: int + size: Optional[str] = None + color: Optional[str] = None + + +class OrderItemResponse(BaseModel): + id: int + product_id: int + quantity: int + price: float + size: Optional[str] + color: Optional[str] + product: ProductResponse + + class Config: + from_attributes = True + + +class OrderCreate(BaseModel): + shipping_address: str + shipping_city: str + shipping_postal_code: str + shipping_country: str + + +class OrderResponse(BaseModel): + id: int + order_number: str + user_id: int + status: str + total_amount: float + shipping_address: str + shipping_city: str + shipping_postal_code: str + shipping_country: str + created_at: datetime + items: List[OrderItemResponse] + + class Config: + from_attributes = True diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py new file mode 100644 index 0000000..7b65d74 --- /dev/null +++ b/backend/app/schemas/product.py @@ -0,0 +1,60 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + + +class ProductCreate(BaseModel): + name: str + description: str + price: float + discount_price: Optional[float] = None + category_id: int + gender: str # men, women + brand: str + sizes: List[str] + colors: List[str] + stock: int + images: List[str] + is_featured: bool = False + is_on_sale: bool = False + + +class ProductUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + discount_price: Optional[float] = None + category_id: Optional[int] = None + gender: Optional[str] = None + brand: Optional[str] = None + sizes: Optional[List[str]] = None + colors: Optional[List[str]] = None + stock: Optional[int] = None + images: Optional[List[str]] = None + is_featured: Optional[bool] = None + is_on_sale: Optional[bool] = None + + +class ProductResponse(BaseModel): + id: int + name: str + description: str + price: float + discount_price: Optional[float] + category_id: int + gender: str + brand: str + sizes: List[str] + colors: List[str] + stock: int + images: List[str] + is_featured: bool + is_on_sale: bool + created_at: datetime + + class Config: + from_attributes = True + + +class ProductDetailResponse(ProductResponse): + pass diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..16c8a33 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, EmailStr +from datetime import datetime +from typing import Optional + + +class UserBase(BaseModel): + email: EmailStr + full_name: str + + +class UserCreate(UserBase): + password: str + + +class UserUpdate(BaseModel): + full_name: Optional[str] = None + phone: Optional[str] = None + address: Optional[str] = None + city: Optional[str] = None + postal_code: Optional[str] = None + country: Optional[str] = None + + +class UserResponse(UserBase): + id: int + phone: Optional[str] + address: Optional[str] + city: Optional[str] + postal_code: Optional[str] + country: Optional[str] + is_active: bool + created_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/schemas/wishlist.py b/backend/app/schemas/wishlist.py new file mode 100644 index 0000000..3a5ae06 --- /dev/null +++ b/backend/app/schemas/wishlist.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from datetime import datetime +from .product import ProductResponse + + +class WishlistItemResponse(BaseModel): + id: int + user_id: int + product_id: int + created_at: datetime + + class Config: + from_attributes = True + + +class WishlistProductResponse(BaseModel): + product: ProductResponse + added_at: datetime + + class Config: + from_attributes = True diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/__pycache__/__init__.cpython-312.pyc b/backend/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78360c92e2d734ae8d6a8104b348f44318ec4bf6 GIT binary patch literal 180 zcmX@j%ge<81Zi?lGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3U;=N2`x@7Dvn7h z%Ph)?@y| h)K^ZNllDNDoV^t(alXPE=etlNlHx4PR&b+Nh~Oc z0ctJFOinG1iI30B%PfhH*DI*}#bJ}1pHiBWYFESxw1W|di$RQ!%#4hTMa)1J0Ic^g An*aa+ literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/auth.cpython-312.pyc b/backend/app/services/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99d9dca726edc1b414ac0e9ccb2f02704e8eba87 GIT binary patch literal 2910 zcma(TTWlN0agTRAK8_Dbr1h*VHMUh#Xq~2Q5;kdVxPBn10~x5A7A9B_C+ qA4L)W73hx$8KAt75fp7d+ApTcN8zu|-0>(` zXweREyE8j8JG(QpGxw)tGEM+~aQbi7s!qsXkO&~wM-Ki0$U32fDmJN-0;wtmML{}b zhpK8p^>EeJs^LP|!!=v4MhcN?v=H^^upO($3vmzEZKIkfBs@G~C#$JKN+BV_!Zf<9 z(AdNHgOG2xkfsJ3q6zqtY iKI>NFv!&IhBfFj5afo%5u zaNz_!;mN9O6gi+V3L0Z{jHT!}aQvNAmhiYyIsx4A=5TK6?|6V*NJgnCn6Ro$#&D#} z7G_S7(HnJPIW^M;JaL&f>*BIg6YP#al^_1}$5%ObxKt~v0)<^BgjHL10oUGk8HYKt zz+4x0kosa7)um=Ogag_Ux4got%$0G<>YSCqJe8@sZCbTb-E`emhf|qeG2InL4;OUK zM1$ALeZS%1i3Xnv-@$np)`@`8hYGG9gBEEif!7zvC(1k=#i*~+QrW+k)NeCxEj4o? zJ_M?K7zP)u&CEQzarw0(Udt`g+ZMNrH)`w(w{ElI73SU&PQAEn31$`-xmly%sG6=| zytrtVZ?PIJn)P}SRBu~l<`%&PE9dJ?nf7fBuE)p0Py#KUfT2zHjnS@gvSXawsBJmB z#)Td2!T~Udj0PSmD@gYNuj@JD>wx@kM?C23E)bpt1-LO OdyYNF1OktZ2n6%G#medght|PdQT_eR9DM(wCsm$<0l`M z9&2a!M^3EI-J9Es4&TprGjpBH-0xPpue{ZH m_WZCev`FD{BFIn6bUR&}yVNT z`x~nX3!8cg1^7h}_$GYrH5lNv8p&VfKgi#|^2-ZdW2R%we4PEw 7CdFRt?_ob_ym##h@ySA6k>>26yb??#noom9YVqW! a*zgz(m7l7x;A` zG0g67VSv Vxi`P%mE ;^yTZfoc1+ zH+^>I0I6P7QD!$)T$ 15{wz pjBaH Fl(~K|ckNNr7osna*>d^$%s|8d^6K6fD?>!m| zHhF%{3w$~&sWR}pntVy~YvMt$0W<<232{jfR_W3t(C}-NOC>b~(r{BMY2Ba^242su zwUE~hyxxLa>G>NTQ4wtKv}PE3ZcY(xdM?RKTf% NmwyBf@MOiP+t z>-&v)6~18|>C(Ij%Oo>Xwo9F{y?H}p30<{4Of&OrF6F}g>Wu;w;kOeI@Oq0R4Ya+P zO^vO(H0^GmLr7~@+OuC;6s{d3q^aL<>#m7g_3=$t(Pf-A(JJ1G!lyz*4`d`QHzX}u zbTwuOc!pq0(ik;X=kid4+oF3`a7`1%HKhm)w^AhAeLb(S0>jI)J+peo)EF1p?pd7~ zrY$B_xZYGQo7GL*%d}ah8CN$&hGFeYq$zNvfoB>S8TsMTxl{2sz$->vy`i&A{L-8@ z!Sowid_ps>nYsD+72VX5@fnt!Q%_`*2J9D~Nv5u8b80*}KOYCx8+uAJ;<##bzF_xU z(ac1SB~m;l63Ax#uo+|%F@6W#0;$TuJ7Y^@A5MI5x+0HQ^2jHV^})5l?SAX%|Bh$LvuNH9;Lx|^yDj6uzvN$jxhxG-< # z9oT``6pYTMntlV87k)gtK7t6kOU!0$6-iOpsz=pL5j?hA3m1cC;&t=Z@NF8`Yiq4p zX*_IOD~f3jEqmD*iM#Xyd!_hQbfr6R+w{6R>UH3@>4iJ$b>O1jR{a*zz5BHqF@2L3 zDLY%FZwu+j{#kIA4%~MjM^dgt^LD@o7%*+Eu>;Q<#J9Fqqz&J|3_4BBCN8KhRp@;C zx&RhE+qMvVHum~N_=9lM4fYJ}pHp=2+V9CM-o1hw@?4y
xQY%T}fd2*gnS=7#fJ8fELi$0h}oV$|TI3lifc74(?*qpj#jh<)M|C z)zOlCd|{#{5ubcVT9Q5-C`r#e@`aWTJPbrCfk7)UxJuW(YhEib$~lLuz@gQb*Uzk- zu>vmv=g#X(udiIb|JJ>?tiVt4dunNFMZKTCm$m}Oc6@HX|DOTk4=we5&|m8&?w-FN zAW~md>Z?e@UrWQQN7qlRov@@AzmbObVa2b@(&1`o@PRb=m+*iU9$Ss9_pkLo2#;+{ zd^Wi`xpfhE3zJpqdH?*a^NT0Tv=7IVw P;&x&Qv)G9xd{TJ0D6UpK7>I=pKT8Pa9Ks?0Nfds@3F!WN2!BAw`3>2D zW-#&hKhG lZmF}R6NU}e3r?#}$8m}p|p6IKA?E^HB}2^)ni z6{#Hy5S6(9*kK$sGhLyMI~C*}$L=U}23~M0g~N^uxOaKJvll@A5>8A(w?Jwh(%pMk z`lIymKq-8r5>~9R^69b9p5J`_i$Ux7Z%X0wmGEm;__b1aYGLwQIZ_SvKK2IRfA{zA zR)?OupSYJ;^p&M&9zRj)d<_;n9jU`hK%pRjBQagt+MZJFKNNcf*QPji{<7lzQ8(Hi zRm*5lVWB6Ldd r!ra+ieapRF}Kh+#wF ef1!2RcGr3%bVF>bLvOyf-)5x#k2kBR^eLka|W~YFM-Z3!6YoZ_s-;pDa zNYo >#r? zCJ1MR<^CE$y83g6Zj5bDY@XiXyqXjcpil_aM5Kp~IP`^6oIZ937q43G{v9_6Yr|*; dECpm+ M-aAHUdgoF?p2PIJ{Esz5FCcyX{>L^~e%=j+E#%G83&aAO6 zQqY(hT2!E#6sl$_NN5dWQq%nzKS7(;PW$7lne5(*inQ%V|G8z`4}R@=&N-VbZnd91 zCHFk<^YK3K``q*8zxaF}0?$zUKV!cW2>ClM?8m)=JpB_O3q&FklOPHcW*EdB35Vhg zI~6v}DqNUT_%N@y!Y+&FObCiQ>{dKskF{nKqT&sEEu2gE!amUF6E#XrxCUr0nUMs! zR&vX3$s>DZXEWHF6eRI78?LjsUf@ V`zLv+251ds=~}t*ISoOo18!i3 z3)O#xmkKea>y)gju~ag|ncQ(I$<%aP;E*OOCOaIVnknGhTDwlrl$4HYA%`hiQo~e^ zXtG&bRw)r+U*Y()2HQpw$8aHmlonHD(;=NTU1?RO6EVs3q|o35Fe4^UWi3sUQ74{j z D$ZA^Hl{P@I|(W4+aKI!1H89_&pLG{ z_3F;J6)w*S0ID)G5E`@WIWo&tmlz>G`2``fyw1nHt95ZT>(X8EnzeMWSCyU=fnK*( zepb*~T>u;YwY&`YCgTHJtj@&)Yx%I_x+>JFds#M`s!9jfQYFVCXM30-SAWS8$*FC! z<>D `pz5*eGt7O8e+w7}I+t_p8 zjKuqGY6Y(14_4?lwkj$-r%1?mjADU<^6Z%|6<@5J!i97ssl~JzlbfcosBE%oEF+s- zG?hrv414^CBi$Kq$C(bLL+Uu#akS&qGp741EPSSGT24x_ #9lWA#FZZbwy z&E#UxL{u uf)^|`Fn4J`AW*DE zrORO*G&xJwbfY&)vMY_xau3>Qeg1iNVz_VoHE5CQxO6c_6XVB|@(7Jxl*dP8^}Lpv z9zO?)k@2%Ml9alYhzf2WKO2dj2M3OWAICxUBE*C`ZrSabo-w^qt4~i@1iBY2VvAEf z5Lt7iAo{NDzOwsQBX94|i>-#(dZ+Qjjkh;0uQ%Fu=fpjEvELB;bK>CK$me41b!{Q@ zZl+M%@T8$}?nq&MbCzv<5@=os-w1z0L{6M{7yKLY{&vIPKJO}Yy_oMB_^fN-x+`C| z?X$XV56*p}JXGeng3x%AyXDTe?l9o5X@?=~nD;&rn)1SCL)cshwtdj`URR;1E#K5- zG<6jM!9wGvg=0646#^{_6E`Nl7Wp>MzX|Uaiz3)82FbeMV&@0F@Ac+_+w;MlMsVly zPjkVMZ1d=YlYi7c$vn*bLC=q!HpWh8Uk_)`gtKEm&W}Zmu}E$#nmr(88|1ko|M1oq z3Gd?zb?Xa(wnA+ayuWq>q39uE;9BpM-kfIxSoZOAqs2PHx!yc*>A?KUmyZ@${>_7z z4qiU|B+&5 &vFC)b{Y1`xGVeca_)q8jXTX(0?KR(9zMIXr`i$lt zL+E+OS9F7|Vhx@I%wK&y$F9q>n+$f-;#9t+$7tzU-un9&K7PSy*^^`Y^6a3&4nAUs zpTKDkT{?962YBqmmk!UTAF+XgST}d%DTD-}*pZ{ Yn(It&tc=kIV;_wDZGUB;HaocL =WU#;b%*Ypcuu0)JhG~{VvYZwM|axO08CWQQ>5e@)(Qgacoc^#ic<}AN2~T@ z1eI7gE2wy6EY+oGYDiDx@0yi>Ds@ge3L@Co)tBKlN51fGxH)u7$azC^qo0d)R^e=( z8@+r4)316STJ8$1Gi%ffiAW+kB`Y&MDXLIRLem2UzDG$x%Th~`Z#6gUz*bi)wYho} zQc*Laf-3yRqFC=K{$??GZDtz!KNXRJb8o55I%7%A>I{}UOg0gNev9ASbOK^si{gx4 z`k0ta&xE~jkaR-cPmjV9KaTnvcom%t!#pNiz9a*GB?DiQzQ<(4--!5_Y%22IO#SuK zMFQZKYJvNr1wIwBCx7(k+Na1V{tWa7n47L5LAdl%2`=xxKl*Y1Q;Sv<7-nciVu)F4 qM~NY3c`I^ `JZ+SBAqRttHux;wnmIBY;X 9 zkxFDxfD9NYi4>6n7_fq*hzbOV3 ?Vh%2(qUb~3E@goNe(KEeR}9-4 zD7pl9W@mQicE9=A{iECMB+xDo{7qHug!~l;wboH($^nLuheReaqmei>!!XF(G+UgV zVdLBkXN_5nj|($GT$~ZDF{jz%ju}VXIpbuAqv sH*bq8nr#a<3)rQC#v5_u!O|!5%tS`B2|?2vdkL#A!v>)np=MGsG#H zl+&@4VIPgAl!YW+1);kt9Hj~f>%*JNG7Le_T~~Ej@%C~Gj6}6b!zRxeQmkrVR+SB5 zK}pC8HN<3MR*!0mVV?(6X{yKur!T3?2Cu1cHDw4?0cj#8;?Am#1Iog`ykj{tLl%Ly zwv??Vd0f@5i5Z6WXT(LmrfJZ|>_wp=t#KJ<=@mkn?AV^e4uoK9%h+1%u=oD^eypHA zYAM6E*x{bTj_arm9A&r`JL00Z%`Col@ meZ|=jU}LMSVDnjcQuRP7#;JTI$B32Rl51HIsG$ zr=uTER@<{*jb4h})D@~n af4a zTr{=>!5WD!FGpbMiW*b&$Z{nshL%?i*MgFot&Vm20(=gGReuECIx#zl+q-csckRKu zMOScr{4Zih$=ChR^|9--clU$m3c+)`Zxw=X6n#I>^52T?y6*GapWNPADFiPReLu_c zCDD@?`|8U5;BX;0ygO0|UMl)tMYT6CzR;w$>|QGb!$sd~S^iM+ZG>~-Ev_g9_oab? zG_XBhlm_>up@KA2l+NY3b7j~p;9orjBxtOjKA@;4Towio$@3=M|C(rv4&2qz)(9wy z;TGa8CtCqUyFp;spt$h!D%DdVTO~$ <$)Ag^SW_`_gDZ8ZAoWd2XB@N1I|5x+Az@DX|LG zXTUrpsd|}`8I@$1Mbx#l+WEd =T6xLIDwKORDnpfyCF59yMXJnGMZXhbE0FfXcMPXIR}XtU3FKG>Ur)@s7HUd5 z2ufU`hoM_1PyK;{e{gH$^YAC(PhSUieWJt(zqxYnO7_wdt_S%y@4b2dEo3I{O=Ryr z;rdEacb@C6K<%kTY1+DOV$CSl5zoV?ni0 y~bu(omnLrTRRWP%vW`M+o4~> zc8uJDS49MDu5oL8s_umho8dD2 *iokYEKbeF+vDJXEi^Zipa{rc#ui1|fy3i@+?aUSclLGuVZo!$eUu z$d{F c g4h%YFaAfDA4w{V zQ3SUvL-%i8Y|Gfd#S8%793HR&+mY~Zn_>;utl9wfD>oqWu|^nztY`|{3W#hi8!S)} zPaVJ2fUh#O4&Mb(j^M1b(6xcD>#!2}*~KSZZzBsNuNC%Ior~e&YYi_p3y8B&SN@+> zxn1naOosUsUeUj+$7s_N3PY`>J)e!ckVUnaU&I@|r8OMa_MuYWOS^|k&81egd&ayJ za 9au8?UZJY3xqT?>c=I z)W3kgo`7zh9NL{5_MH8}v7){Az~RmHJau($T+dzKcbzD>PHg+PFKnOQ@o!~{uCrgg z|Ml9Vwf*7Ch2hJ4@9d@b7XI+gUbr|sRd7v}S)0@OPk}gnxt>k#!{Y}IPp&(gM#h!v z{IJ{f+IhhWoscjIjbiot4qXEmBk~l>aj47i= !?z% zu#ix!C=~O;!ib(i4zE+I{yejXVFPJRd7X}d3@=apBJ@x+80OEU|A4%5K*9&)Mbk<8 zUoIU#Svt{Q`q4ni=P&s?zxQ@AKPlP0CJXe?-@7&NanR&}hic>Q-ki?;(iCxIC!U^7 zId|K1;K)h5$2L>Bgel?3MI?{uM$SWAUK1^vJ|Z}?r|;b=`*AeHFjttZ( qgUwe z2YX^+ b%7 literal 0 HcmV?d00001 diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py new file mode 100644 index 0000000..a5db368 --- /dev/null +++ b/backend/app/services/auth.py @@ -0,0 +1,52 @@ +from datetime import datetime, timedelta +from typing import Optional +from passlib.context import CryptContext +from jose import JWTError, jwt +from app.config import settings +from app.models import User +from sqlalchemy.orm import Session + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.access_token_expire_minutes + ) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm + ) + return encoded_jwt + + +def verify_token(token: str) -> Optional[int]: + try: + payload = jwt.decode( + token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm] + ) + user_id: int = payload.get("sub") + if user_id is None: + return None + return user_id + except JWTError: + return None + + +def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: + user = db.query(User).filter(User.email == email).first() + if not user or not verify_password(password, user.hashed_password): + return None + return user diff --git a/backend/app/services/cart.py b/backend/app/services/cart.py new file mode 100644 index 0000000..1ce53ee --- /dev/null +++ b/backend/app/services/cart.py @@ -0,0 +1,82 @@ +from sqlalchemy.orm import Session +from app.models import Cart, CartItem, Product +from app.schemas.cart import CartItemCreate, CartItemUpdate +from typing import Optional +import uuid + + +def get_or_create_cart(db: Session, user_id: int) -> Cart: + cart = db.query(Cart).filter(Cart.user_id == user_id).first() + if not cart: + cart = Cart(user_id=user_id) + db.add(cart) + db.commit() + db.refresh(cart) + return cart + + +def add_to_cart(db: Session, user_id: int, item: CartItemCreate) -> CartItem: + cart = get_or_create_cart(db, user_id) + + # Check if item already exists + existing_item = ( + db.query(CartItem) + .filter( + CartItem.cart_id == cart.id, + CartItem.product_id == item.product_id, + CartItem.size == item.size, + CartItem.color == item.color, + ) + .first() + ) + + if existing_item: + existing_item.quantity += item.quantity + db.commit() + db.refresh(existing_item) + return existing_item + + cart_item = CartItem(cart_id=cart.id, **item.dict()) + db.add(cart_item) + db.commit() + db.refresh(cart_item) + return cart_item + + +def get_cart(db: Session, user_id: int) -> Optional[Cart]: + return db.query(Cart).filter(Cart.user_id == user_id).first() + + +def update_cart_item( + db: Session, cart_item_id: int, update: CartItemUpdate +) -> Optional[CartItem]: + cart_item = db.query(CartItem).filter(CartItem.id == cart_item_id).first() + if not cart_item: + return None + + if update.quantity: + cart_item.quantity = update.quantity + + db.commit() + db.refresh(cart_item) + return cart_item + + +def remove_from_cart(db: Session, cart_item_id: int) -> bool: + cart_item = db.query(CartItem).filter(CartItem.id == cart_item_id).first() + if not cart_item: + return False + + db.delete(cart_item) + db.commit() + return True + + +def clear_cart(db: Session, user_id: int) -> bool: + cart = get_cart(db, user_id) + if not cart: + return False + + db.query(CartItem).filter(CartItem.cart_id == cart.id).delete() + db.commit() + return True diff --git a/backend/app/services/order.py b/backend/app/services/order.py new file mode 100644 index 0000000..18e72bc --- /dev/null +++ b/backend/app/services/order.py @@ -0,0 +1,73 @@ +from sqlalchemy.orm import Session +from app.models import Order, OrderItem, Cart, CartItem, Product +from app.schemas.order import OrderCreate, OrderItemCreate +from typing import Optional +import uuid +from datetime import datetime + + +def create_order(db: Session, user_id: int, order_data: OrderCreate) -> Optional[Order]: + cart = db.query(Cart).filter(Cart.user_id == user_id).first() + if not cart or not cart.items: + return None + + total_amount = 0 + order_items_data = [] + + for cart_item in cart.items: + product = cart_item.product + price = product.discount_price if product.discount_price else product.price + total_amount += price * cart_item.quantity + + order_items_data.append({ + "product_id": product.id, + "quantity": cart_item.quantity, + "price": price, + "size": cart_item.size, + "color": cart_item.color, + }) + + order_number = f"ORD-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}" + + order = Order( + user_id=user_id, + order_number=order_number, + status="pending", + total_amount=total_amount, + **order_data.dict(), + ) + + db.add(order) + db.flush() + + for item_data in order_items_data: + order_item = OrderItem(order_id=order.id, **item_data) + db.add(order_item) + product = db.query(Product).filter(Product.id == item_data["product_id"]).first() + product.stock -= item_data["quantity"] + + # Clear cart + db.query(CartItem).filter(CartItem.cart_id == cart.id).delete() + + db.commit() + db.refresh(order) + return order + + +def get_order_by_id(db: Session, order_id: int) -> Optional[Order]: + return db.query(Order).filter(Order.id == order_id).first() + + +def get_user_orders(db: Session, user_id: int) -> list: + return db.query(Order).filter(Order.user_id == user_id).all() + + +def update_order_status(db: Session, order_id: int, status: str) -> Optional[Order]: + order = get_order_by_id(db, order_id) + if not order: + return None + + order.status = status + db.commit() + db.refresh(order) + return order diff --git a/backend/app/services/product.py b/backend/app/services/product.py new file mode 100644 index 0000000..03c1dd6 --- /dev/null +++ b/backend/app/services/product.py @@ -0,0 +1,74 @@ +from sqlalchemy.orm import Session +from app.models import Product, Category +from app.schemas.product import ProductCreate, ProductUpdate +from typing import List, Optional + + +def get_products( + db: Session, + category_id: Optional[int] = None, + gender: Optional[str] = None, + on_sale: Optional[bool] = None, + featured: Optional[bool] = None, + skip: int = 0, + limit: int = 10, +) -> List[Product]: + query = db.query(Product) + + if category_id: + query = query.filter(Product.category_id == category_id) + if gender: + query = query.filter(Product.gender == gender) + 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() + + +def get_product_by_id(db: Session, product_id: int) -> Optional[Product]: + return db.query(Product).filter(Product.id == product_id).first() + + +def create_product(db: Session, product: ProductCreate) -> Product: + db_product = Product(**product.dict()) + db.add(db_product) + db.commit() + db.refresh(db_product) + return db_product + + +def update_product(db: Session, product_id: int, product_update: ProductUpdate) -> Optional[Product]: + db_product = get_product_by_id(db, product_id) + if not db_product: + return None + + for field, value in product_update.dict(exclude_unset=True).items(): + setattr(db_product, field, value) + + db.commit() + db.refresh(db_product) + return db_product + + +def delete_product(db: Session, product_id: int) -> bool: + db_product = get_product_by_id(db, product_id) + if not db_product: + return False + + db.delete(db_product) + db.commit() + return True + + +def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]: + return ( + db.query(Product) + .filter( + Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%") + ) + .offset(skip) + .limit(limit) + .all() + ) diff --git a/backend/insert_products.py b/backend/insert_products.py new file mode 100644 index 0000000..ffbd733 --- /dev/null +++ b/backend/insert_products.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +Insert sample products into the database using SQLAlchemy ORM +""" +from app.database.database import SessionLocal +from app.models.product import Product +from app.models.category import Category + +def seed_products(): + db = SessionLocal() + + try: + # Get categories + category_shoes = db.query(Category).filter(Category.slug == "shoes").first() + category_shirts = db.query(Category).filter(Category.slug == "shirts").first() + category_pants = db.query(Category).filter(Category.slug == "pants").first() + + if not category_shoes: + print("ERROR: Categories not found. Run schema.sql first!") + return + + # Clear existing products + db.query(Product).delete() + db.commit() + print("✓ Cleared existing products") + + # Products data + products = [ + # Shoes + Product( + name="Premium Running Shoes", + description="High-performance running shoes with advanced cushioning technology", + price=129.99, + discount_price=99.99, + category_id=category_shoes.id, + gender="men", + brand="Nike", + sizes=["7", "8", "9", "10", "11", "12", "13"], + colors=["Black", "White", "Blue"], + stock=50, + images=["https://via.placeholder.com/300x300?text=Nike+Running"], + is_featured=True, + is_on_sale=True + ), + Product( + name="Women Athletic Sneakers", + description="Comfortable athletic sneakers for everyday wear", + price=99.99, + discount_price=None, + category_id=category_shoes.id, + gender="women", + brand="Adidas", + sizes=["5", "6", "7", "8", "9", "10"], + colors=["Pink", "White", "Purple"], + stock=45, + images=["https://via.placeholder.com/300x300?text=Adidas+Sneakers"], + is_featured=True, + is_on_sale=False + ), + Product( + name="Basketball High Tops", + description="Professional basketball shoes with ankle support", + price=149.99, + discount_price=None, + category_id=category_shoes.id, + gender="men", + brand="Jordan", + sizes=["8", "9", "10", "11", "12", "13"], + colors=["Red", "Black", "Gold"], + stock=30, + images=["https://via.placeholder.com/300x300?text=Jordan+High"], + is_featured=True, + is_on_sale=False + ), + Product( + name="Casual Leather Loafers", + description="Classic leather loafers for formal occasions", + price=139.99, + discount_price=109.99, + category_id=category_shoes.id, + gender="men", + brand="Cole Haan", + sizes=["7", "8", "9", "10", "11", "12"], + colors=["Brown", "Black"], + stock=25, + images=["https://via.placeholder.com/300x300?text=Cole+Haan+Loafers"], + is_featured=True, + is_on_sale=True + ), + Product( + name="Hiking Boot Pro", + description="Durable hiking boots with waterproof technology", + price=179.99, + discount_price=149.99, + category_id=category_shoes.id, + gender="men", + brand="Salomon", + sizes=["8", "9", "10", "11", "12", "13"], + colors=["Brown", "Gray", "Black"], + stock=35, + images=["https://via.placeholder.com/300x300?text=Salomon+Hiking"], + is_featured=True, + is_on_sale=True + ), + # Clothing + Product( + name="Classic Cotton T-Shirt", + description="Comfortable everyday cotton t-shirt", + price=29.99, + discount_price=None, + category_id=category_shirts.id, + gender="men", + brand="Gap", + sizes=["S", "M", "L", "XL", "XXL"], + colors=["Red", "Blue", "White", "Black"], + stock=100, + images=["https://via.placeholder.com/300x300?text=Cotton+Tee"], + is_featured=False, + is_on_sale=False + ), + Product( + name="Silk Blouse", + description="Elegant silk blouse for professional settings", + price=89.99, + discount_price=69.99, + category_id=category_shirts.id, + gender="women", + brand="Hugo Boss", + sizes=["XS", "S", "M", "L", "XL"], + colors=["White", "Black", "Navy"], + stock=40, + images=["https://via.placeholder.com/300x300?text=Silk+Blouse"], + is_featured=False, + is_on_sale=True + ), + Product( + name="Slim Fit Jeans", + description="Modern slim fit jeans with stretch fabric", + price=79.99, + discount_price=59.99, + category_id=category_pants.id, + gender="men", + brand="Levi's", + sizes=["28", "30", "32", "34", "36", "38"], + colors=["Dark Blue", "Light Blue", "Black"], + stock=60, + images=["https://via.placeholder.com/300x300?text=Slim+Jeans"], + is_featured=False, + is_on_sale=True + ), + Product( + name="Yoga Leggings", + description="High-waist yoga leggings with moisture-wicking", + price=89.99, + discount_price=None, + category_id=category_pants.id, + gender="women", + brand="Lululemon", + sizes=["XS", "S", "M", "L", "XL"], + colors=["Black", "Navy", "Purple", "Gray"], + stock=55, + images=["https://via.placeholder.com/300x300?text=Yoga+Leggings"], + is_featured=True, + is_on_sale=False + ), + ] + + # Add products + for product in products: + db.add(product) + + db.commit() + print(f"✓ Added {len(products)} products") + print("\n✅ Database seeded successfully!") + + except Exception as e: + db.rollback() + print(f"❌ Error: {e}") + finally: + db.close() + +if __name__ == "__main__": + seed_products() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..669064b --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +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 diff --git a/backend/schema.sql b/backend/schema.sql new file mode 100644 index 0000000..8079bfb --- /dev/null +++ b/backend/schema.sql @@ -0,0 +1,344 @@ +-- 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 + +-- ============================================ +-- CREATE TABLES +-- ============================================ + +-- Category Table +CREATE TABLE category ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + slug VARCHAR(100) UNIQUE NOT NULL, + description TEXT +); + +-- User Table +CREATE TABLE "user" ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + hashed_password VARCHAR(255) NOT NULL, + full_name VARCHAR(255), + phone VARCHAR(20), + address TEXT, + city VARCHAR(100), + postal_code VARCHAR(20), + country VARCHAR(100), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Product Table +CREATE TABLE product ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(10, 2) NOT NULL, + discount_price DECIMAL(10, 2), + category_id INTEGER NOT NULL, + gender VARCHAR(50), + brand VARCHAR(100), + sizes JSONB, + colors JSONB, + stock INTEGER DEFAULT 0, + images JSONB, + is_featured BOOLEAN DEFAULT FALSE, + is_on_sale BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE +); + +-- Cart Table +CREATE TABLE cart ( + id SERIAL PRIMARY KEY, + user_id INTEGER UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE +); + +-- Cart Item Table +CREATE TABLE cart_item ( + id SERIAL PRIMARY KEY, + cart_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER DEFAULT 1, + size VARCHAR(50), + color VARCHAR(50), + FOREIGN KEY (cart_id) REFERENCES cart(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE +); + +-- Order Table +CREATE TABLE "order" ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + order_number VARCHAR(100) UNIQUE NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + total_amount DECIMAL(10, 2) NOT NULL, + shipping_address TEXT, + shipping_city VARCHAR(100), + shipping_postal_code VARCHAR(20), + shipping_country VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE +); + +-- Order Item Table +CREATE TABLE order_item ( + id SERIAL PRIMARY KEY, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + price DECIMAL(10, 2) NOT NULL, + size VARCHAR(50), + color VARCHAR(50), + FOREIGN KEY (order_id) REFERENCES "order"(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE +); + +-- Wishlist (User-Product Association) +CREATE TABLE user_wishlist ( + user_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + PRIMARY KEY (user_id, product_id), + FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE +); + +-- Contact Message Table +CREATE TABLE contact_message ( + id SERIAL PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255), + subject VARCHAR(255), + message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================ +-- CREATE INDEXES +-- ============================================ +CREATE INDEX idx_user_email ON "user"(email); +CREATE INDEX idx_product_name ON product(name); +CREATE INDEX idx_product_category ON product(category_id); +CREATE INDEX idx_product_is_featured ON product(is_featured); +CREATE INDEX idx_product_is_on_sale ON product(is_on_sale); +CREATE INDEX idx_order_user ON "order"(user_id); +CREATE INDEX idx_order_created_at ON "order"(created_at); +CREATE INDEX idx_cart_item_cart ON cart_item(cart_id); +CREATE INDEX idx_order_item_order ON order_item(order_id); + +-- ============================================ +-- POPULATE INITIAL DATA +-- ============================================ + +-- Insert Categories +INSERT INTO category (name, slug, description) VALUES + ('Shoes', 'shoes', 'Footwear for all occasions'), + ('Shirts', 'shirts', 'T-shirts and casual tops'), + ('Pants', 'pants', 'Jeans and trousers'), + ('Hats', 'hats', 'Caps and beanies'), + ('Accessories', 'accessories', 'Bags, belts, and more'); + +-- Insert Sample Products (Shoes - Featured) +INSERT INTO product (name, description, price, discount_price, category_id, gender, brand, sizes, colors, stock, images, is_featured, is_on_sale) VALUES + ( + 'Premium Running Shoes', + 'High-performance running shoes with advanced cushioning technology', + 129.99, + 99.99, + 1, + 'men', + 'Nike', + '["7", "8", "9", "10", "11", "12", "13"]', + '["Black", "White", "Blue"]', + 50, + '["https://via.placeholder.com/300x300?text=Nike+Running"]', + TRUE, + TRUE + ), + ( + 'Women Athletic Sneakers', + 'Comfortable athletic sneakers for everyday wear', + 99.99, + NULL, + 1, + 'women', + 'Adidas', + '["5", "6", "7", "8", "9", "10"]', + '["Pink", "White", "Purple"]', + 45, + '["https://via.placeholder.com/300x300?text=Adidas+Sneakers"]', + TRUE, + FALSE + ), + ( + 'Basketball High Tops', + 'Professional basketball shoes with ankle support', + 149.99, + NULL, + 1, + 'men', + 'Jordan', + '["8", "9", "10", "11", "12", "13"]', + '["Red", "Black", "Gold"]', + 30, + '["https://via.placeholder.com/300x300?text=Jordan+High"]', + TRUE, + FALSE + ), + ( + 'Casual Leather Loafers', + 'Classic leather loafers for formal occasions', + 139.99, + 109.99, + 1, + 'men', + 'Cole Haan', + '["7", "8", "9", "10", "11", "12"]', + '["Brown", "Black"]', + 25, + '["https://via.placeholder.com/300x300?text=Cole+Haan+Loafers"]', + TRUE, + TRUE + ), + ( + 'Hiking Boot Pro', + 'Durable hiking boots with waterproof technology', + 179.99, + 149.99, + 1, + 'men', + 'Salomon', + '["8", "9", "10", "11", "12", "13"]', + '["Brown", "Gray", "Black"]', + 35, + '["https://via.placeholder.com/300x300?text=Salomon+Hiking"]', + TRUE, + TRUE + ); + +-- Insert Sample Products (Clothing) +INSERT INTO product (name, description, price, discount_price, category_id, gender, brand, sizes, colors, stock, images, is_featured, is_on_sale) VALUES + ( + 'Classic Cotton T-Shirt', + 'Comfortable everyday cotton t-shirt', + 29.99, + NULL, + 2, + 'men', + 'Gap', + '["S", "M", "L", "XL", "XXL"]', + '["Red", "Blue", "White", "Black"]', + 100, + '["https://via.placeholder.com/300x300?text=Cotton+Tee"]', + FALSE, + FALSE + ), + ( + 'Silk Blouse', + 'Elegant silk blouse for professional settings', + 89.99, + 69.99, + 2, + 'women', + 'Hugo Boss', + '["XS", "S", "M", "L", "XL"]', + '["White", "Black", "Navy"]', + 40, + '["https://via.placeholder.com/300x300?text=Silk+Blouse"]', + FALSE, + TRUE + ), + ( + 'Slim Fit Jeans', + 'Modern slim fit jeans with stretch fabric', + 79.99, + 59.99, + 3, + 'men', + 'Levi''s', + '["28", "30", "32", "34", "36", "38"]', + '["Dark Blue", "Light Blue", "Black"]', + 60, + '["https://via.placeholder.com/300x300?text=Slim+Jeans"]', + FALSE, + TRUE + ), + ( + 'Yoga Leggings', + 'High-waist yoga leggings with moisture-wicking', + 89.99, + NULL, + 3, + 'women', + 'Lululemon', + '["XS", "S", "M", "L", "XL"]', + '["Black", "Navy", "Purple", "Gray"]', + 55, + '["https://via.placeholder.com/300x300?text=Yoga+Leggings"]', + TRUE, + FALSE + ); + +-- Insert Sample Users (for testing) +INSERT INTO "user" (email, hashed_password, full_name, phone, address, city, postal_code, country, is_active) VALUES + ( + 'user@example.com', + '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123 + 'John Doe', + '1234567890', + '123 Main Street', + 'New York', + '10001', + 'USA', + TRUE + ), + ( + 'jane@example.com', + '$2b$12$KIXxPfROLqWHYIgC5FCOO.7yqKU8RvOmOhP7kJZnYLh6pJn2FSfKy', -- password: password123 + 'Jane Smith', + '9876543210', + '456 Oak Avenue', + 'Los Angeles', + '90001', + 'USA', + TRUE + ); + +-- Create carts for users +INSERT INTO cart (user_id) VALUES + (1), + (2); + +-- ============================================ +-- SET PERMISSIONS +-- ============================================ +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 FUNCTIONS IN SCHEMA public TO ecommerce_user; + +-- ============================================ +-- COMPLETE +-- ============================================ +-- Schema created successfully! +-- +-- User: ecommerce_user +-- Database: ecommerce_db +-- Host: localhost:5432 +-- +-- 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 +-- +-- Sample data: 5 categories, 9 products, 2 users +-- +-- Next steps: +-- 1. Update backend/.env with your credentials +-- 2. Run: uvicorn app.main:app --reload --port 8000 diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..48af734 --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,277 @@ +""" +Seed data for the e-commerce database. +Run this script with: python seed.py +""" + +from sqlalchemy.orm import Session +from app.database.database import SessionLocal, engine, Base +from app.models import Category, Product, User +from app.services.auth import get_password_hash +import json + +# Create tables +Base.metadata.create_all(bind=engine) + + +def seed_database(): + db = SessionLocal() + + # Check if data already exists + if db.query(Category).first(): + print("Database already seeded!") + db.close() + return + + # Create categories + categories = [ + Category(name="Shoes", slug="shoes", description="Footwear for all occasions"), + Category(name="Shirts", slug="shirts", description="Men's and women's shirts"), + Category(name="Pants", slug="pants", description="Trousers and jeans"), + Category(name="Hats", slug="hats", description="Caps, beanies, and more"), + Category(name="Accessories", slug="accessories", description="Belts, scarves, and more"), + ] + + db.add_all(categories) + db.flush() + + # Create sample products (focus on shoes) + products_data = [ + # Shoes + { + "name": "Premium Running Shoes", + "description": "High-performance running shoes with advanced cushioning technology", + "price": 129.99, + "discount_price": 89.99, + "category_id": 1, + "gender": "men", + "brand": "Nike", + "sizes": ["6", "7", "8", "9", "10", "11", "12", "13"], + "colors": ["Black", "White", "Blue", "Red"], + "stock": 50, + "images": ["https://via.placeholder.com/500x500?text=Running+Shoes"], + "is_featured": True, + "is_on_sale": True, + }, + { + "name": "Women's Athletic Sneakers", + "description": "Comfortable and stylish sneakers for daily wear and workout", + "price": 119.99, + "discount_price": None, + "category_id": 1, + "gender": "women", + "brand": "Adidas", + "sizes": ["5", "6", "7", "8", "9", "10", "11"], + "colors": ["Pink", "Purple", "White", "Black"], + "stock": 35, + "images": ["https://via.placeholder.com/500x500?text=Womens+Sneakers"], + "is_featured": True, + "is_on_sale": False, + }, + { + "name": "Casual Leather Loafers", + "description": "Elegant leather loafers perfect for office or casual outings", + "price": 159.99, + "discount_price": 129.99, + "category_id": 1, + "gender": "men", + "brand": "Cole Haan", + "sizes": ["7", "8", "9", "10", "11", "12", "13"], + "colors": ["Brown", "Black", "Tan"], + "stock": 25, + "images": ["https://via.placeholder.com/500x500?text=Loafers"], + "is_featured": True, + "is_on_sale": True, + }, + { + "name": "Summer Flip Flops", + "description": "Comfortable flip flops for beach and casual summer wear", + "price": 29.99, + "discount_price": 19.99, + "category_id": 1, + "gender": "women", + "brand": "Havaianas", + "sizes": ["6", "7", "8", "9", "10"], + "colors": ["Turquoise", "Pink", "Yellow", "White"], + "stock": 100, + "images": ["https://via.placeholder.com/500x500?text=Flip+Flops"], + "is_featured": False, + "is_on_sale": True, + }, + { + "name": "Basketball High Tops", + "description": "Professional basketball shoes with superior ankle support", + "price": 179.99, + "discount_price": 149.99, + "category_id": 1, + "gender": "men", + "brand": "Jordan", + "sizes": ["7", "8", "9", "10", "11", "12", "13", "14"], + "colors": ["Black", "Red", "White", "Gold"], + "stock": 40, + "images": ["https://via.placeholder.com/500x500?text=Basketball+Shoes"], + "is_featured": True, + "is_on_sale": False, + }, + { + "name": "Hiking Boot Pro", + "description": "Durable hiking boots with waterproof protection and excellent grip", + "price": 189.99, + "discount_price": 149.99, + "category_id": 1, + "gender": "men", + "brand": "Salomon", + "sizes": ["6", "7", "8", "9", "10", "11", "12", "13"], + "colors": ["Brown", "Gray", "Black"], + "stock": 30, + "images": ["https://via.placeholder.com/500x500?text=Hiking+Boots"], + "is_featured": True, + "is_on_sale": True, + }, + { + "name": "Evening Heels", + "description": "Elegant high heels for special occasions", + "price": 139.99, + "discount_price": None, + "category_id": 1, + "gender": "women", + "brand": "Jimmy Choo", + "sizes": ["5", "6", "7", "8", "9", "10"], + "colors": ["Black", "Silver", "Gold", "Red"], + "stock": 20, + "images": ["https://via.placeholder.com/500x500?text=Evening+Heels"], + "is_featured": False, + "is_on_sale": False, + }, + { + "name": "Casual Canvas Shoes", + "description": "Lightweight canvas shoes perfect for everyday casual wear", + "price": 59.99, + "discount_price": 39.99, + "category_id": 1, + "gender": "men", + "brand": "Vans", + "sizes": ["6", "7", "8", "9", "10", "11", "12", "13"], + "colors": ["White", "Black", "Blue", "Red"], + "stock": 60, + "images": ["https://via.placeholder.com/500x500?text=Canvas+Shoes"], + "is_featured": False, + "is_on_sale": True, + }, + # Shirts + { + "name": "Classic Cotton T-Shirt", + "description": "High-quality cotton t-shirt comfortable for everyday wear", + "price": 29.99, + "discount_price": None, + "category_id": 2, + "gender": "men", + "brand": "Gap", + "sizes": ["XS", "S", "M", "L", "XL", "XXL"], + "colors": ["White", "Black", "Blue", "Gray"], + "stock": 80, + "images": ["https://via.placeholder.com/500x500?text=T-Shirt"], + "is_featured": False, + "is_on_sale": False, + }, + { + "name": "Silk Blouse", + "description": "Elegant silk blouse for professional and casual occasions", + "price": 89.99, + "discount_price": 59.99, + "category_id": 2, + "gender": "women", + "brand": "Hugo Boss", + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": ["White", "Black", "Blue", "Burgundy"], + "stock": 25, + "images": ["https://via.placeholder.com/500x500?text=Blouse"], + "is_featured": True, + "is_on_sale": True, + }, + # Pants + { + "name": "Slim Fit Jeans", + "description": "Modern slim fit jeans with stretch comfort", + "price": 79.99, + "discount_price": 59.99, + "category_id": 3, + "gender": "men", + "brand": "Levi's", + "sizes": ["28", "30", "32", "34", "36", "38", "40"], + "colors": ["Dark Blue", "Light Blue", "Black"], + "stock": 50, + "images": ["https://via.placeholder.com/500x500?text=Jeans"], + "is_featured": False, + "is_on_sale": True, + }, + { + "name": "Yoga Leggings", + "description": "High-waist yoga leggings with moisture-wicking fabric", + "price": 69.99, + "discount_price": None, + "category_id": 3, + "gender": "women", + "brand": "Lululemon", + "sizes": ["XS", "S", "M", "L", "XL"], + "colors": ["Black", "Navy", "Burgundy", "Gray"], + "stock": 35, + "images": ["https://via.placeholder.com/500x500?text=Leggings"], + "is_featured": True, + "is_on_sale": False, + }, + ] + + for product_data in products_data: + product = Product( + name=product_data["name"], + description=product_data["description"], + price=product_data["price"], + discount_price=product_data["discount_price"], + category_id=product_data["category_id"], + gender=product_data["gender"], + brand=product_data["brand"], + sizes=product_data["sizes"], + colors=product_data["colors"], + stock=product_data["stock"], + images=product_data["images"], + is_featured=product_data["is_featured"], + is_on_sale=product_data["is_on_sale"], + ) + db.add(product) + + # Create sample users + sample_users = [ + User( + email="user@example.com", + full_name="John Doe", + hashed_password=get_password_hash("password123"), + phone="+1234567890", + address="123 Main St", + city="New York", + postal_code="10001", + country="USA", + ), + User( + email="jane@example.com", + full_name="Jane Smith", + hashed_password=get_password_hash("password123"), + phone="+0987654321", + address="456 Oak Ave", + city="Los Angeles", + postal_code="90001", + country="USA", + ), + ] + + db.add_all(sample_users) + + db.commit() + print("Database seeded successfully!") + print(f"Created {len(categories)} categories") + print(f"Created {len(products_data)} products") + print(f"Created {len(sample_users)} users") + db.close() + + +if __name__ == "__main__": + seed_database() diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..63f5530 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:8000/api +VITE_APP_NAME=StyleHub diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3b7d53e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + StyleHub - Fashion & Shoe Store + + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..c08f7c6 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1900 @@ +{ + "name": "ecommerce-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ecommerce-frontend", + "version": "0.0.1", + "dependencies": { + "axios": "^1.6.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.340", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", + "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9fb5479 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "ecommerce-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "axios": "^1.6.5" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.8" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..febeb1b --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider } from './context/AuthContext' +import { CartProvider } from './context/CartContext' +import Navbar from './components/Navbar' +import Footer from './components/Footer' + +// Pages +import Home from './pages/Home' +import Products from './pages/Products' +import ProductDetail from './pages/ProductDetail' +import Cart from './pages/Cart' +import Checkout from './pages/Checkout' +import Login from './pages/Login' +import Register from './pages/Register' +import Profile from './pages/Profile' +import Orders from './pages/Orders' +import Wishlist from './pages/Wishlist' +import About from './pages/About' +import Contact from './pages/Contact' +import Sales from './pages/Sales' + +function App() { + return ( ++ + ) +} + +export default App diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..3ba4d48 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,19 @@ +import axios from 'axios' + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api' + +const api = axios.create({ + baseURL: API_URL, +}) + +// Add token to requests +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) { + config.params = config.params || {} + config.params.token = token + } + return config +}) + +export default api diff --git a/frontend/src/components/CategoryCard.jsx b/frontend/src/components/CategoryCard.jsx new file mode 100644 index 0000000..24165d9 --- /dev/null +++ b/frontend/src/components/CategoryCard.jsx @@ -0,0 +1,14 @@ +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}` + + return ( + ++ ++ ++++ + + ++ +} /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
{category.name}
+{category.description}
+ + ) +} diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..6037da6 --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,54 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import '../styles/global.css' + +export default function Footer() { + return ( + + ) +} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 0000000..8fe840d --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,72 @@ +import React, { useContext } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { AuthContext } from '../context/AuthContext' +import { CartContext } from '../context/CartContext' +import SearchBar from './SearchBar' +import '../styles/global.css' + +export default function Navbar() { + const { user, token, logout } = useContext(AuthContext) + const { cart } = useContext(CartContext) + const navigate = useNavigate() + + const handleLogout = () => { + logout() + navigate('/') + } + + return ( + + ) +} diff --git a/frontend/src/components/ProductCard.jsx b/frontend/src/components/ProductCard.jsx new file mode 100644 index 0000000..533b3bb --- /dev/null +++ b/frontend/src/components/ProductCard.jsx @@ -0,0 +1,51 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import '../styles/global.css' + +export default function ProductCard({ product }) { + const price = product.discount_price || product.price + const discount = + product.discount_price && product.is_on_sale + ? Math.round( + ((product.price - product.discount_price) / product.price) * 100 + ) + : 0 + + return ( +++ ) +} diff --git a/frontend/src/components/ProductFilters.jsx b/frontend/src/components/ProductFilters.jsx new file mode 100644 index 0000000..71139a3 --- /dev/null +++ b/frontend/src/components/ProductFilters.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' +import '../styles/global.css' + +export default function ProductFilters({ onFilter }) { + const [filters, setFilters] = useState({ + gender: '', + priceRange: 'all', + onSale: false, + }) + + const handleFilterChange = (key, value) => { + const newFilters = { ...filters, [key]: value } + setFilters(newFilters) + onFilter(newFilters) + } + + return ( ++++ {product.is_on_sale && discount > 0 && ( +
{discount}% OFF+ )} + {product.is_featured && ( +FEATURED+ )} +++{product.name}
+{product.brand}
++ {product.discount_price ? ( + <> + ${product.price.toFixed(2)} + ${product.discount_price.toFixed(2)} + > + ) : ( + ${price.toFixed(2)} + )} +++ {product.stock > 0 ? ( + In Stock + ) : ( + Out of Stock + )} +
+ + View Details + +++ ) +} diff --git a/frontend/src/components/SearchBar.jsx b/frontend/src/components/SearchBar.jsx new file mode 100644 index 0000000..8c87531 --- /dev/null +++ b/frontend/src/components/SearchBar.jsx @@ -0,0 +1,50 @@ +import React, { useState, useContext } from 'react' +import { AuthContext } from '../context/AuthContext' +import api from '../api' +import '../styles/global.css' + +export default function SearchBar() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [showResults, setShowResults] = useState(false) + + const handleSearch = async (e) => { + const value = e.target.value + setQuery(value) + + if (value.length > 2) { + try { + const response = await api.get('/products/search', { + params: { q: value }, + }) + setResults(response.data) + setShowResults(true) + } catch (error) { + console.error('Search error:', error) + } + } else { + setShowResults(false) + } + } + + return ( +Filters
+ ++ + ++ ++ + ++ ++ ++ + ++ + {showResults && results.length > 0 && ( ++ ) +} diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx new file mode 100644 index 0000000..49f4ba6 --- /dev/null +++ b/frontend/src/context/AuthContext.jsx @@ -0,0 +1,29 @@ +import React, { createContext, useState, useEffect } from 'react' + +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) + + useEffect(() => { + if (token) { + localStorage.setItem('token', token) + } else { + localStorage.removeItem('token') + } + }, [token]) + + const logout = () => { + setUser(null) + setToken(null) + localStorage.removeItem('token') + } + + return ( ++ {results.map((product) => ( + + {product.name} + ${product.price.toFixed(2)} + + ))} ++ )} ++ {children} + + ) +} diff --git a/frontend/src/context/CartContext.jsx b/frontend/src/context/CartContext.jsx new file mode 100644 index 0000000..1a3aebb --- /dev/null +++ b/frontend/src/context/CartContext.jsx @@ -0,0 +1,76 @@ +import React, { createContext, useState, useEffect } from 'react' + +export const CartContext = createContext() + +export const CartProvider = ({ children }) => { + const [cart, setCart] = useState([]) + const [total, setTotal] = useState(0) + + useEffect(() => { + calculateTotal() + }, [cart]) + + const calculateTotal = () => { + const newTotal = cart.reduce((sum, item) => { + const price = item.product.discount_price || item.product.price + return sum + price * item.quantity + }, 0) + setTotal(newTotal) + } + + const addToCart = (product, quantity = 1, size = null, color = null) => { + const existingItem = cart.find( + (item) => + item.product.id === product.id && + item.size === size && + item.color === color + ) + + if (existingItem) { + setCart( + cart.map((item) => + item === existingItem + ? { ...item, quantity: item.quantity + quantity } + : item + ) + ) + } else { + setCart([...cart, { product, quantity, size, color }]) + } + } + + const removeFromCart = (index) => { + setCart(cart.filter((_, i) => i !== index)) + } + + const updateQuantity = (index, quantity) => { + if (quantity <= 0) { + removeFromCart(index) + } else { + setCart( + cart.map((item, i) => + i === index ? { ...item, quantity } : item + ) + ) + } + } + + const clearCart = () => { + setCart([]) + } + + return ( ++ {children} + + ) +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..9e76786 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './styles/global.css' + +ReactDOM.createRoot(document.getElementById('root')).render( ++ , +) diff --git a/frontend/src/pages/About.jsx b/frontend/src/pages/About.jsx new file mode 100644 index 0000000..c7560ea --- /dev/null +++ b/frontend/src/pages/About.jsx @@ -0,0 +1,90 @@ +import React from 'react' +import '../styles/global.css' + +export default function About() { + return ( ++ ++ ) +} diff --git a/frontend/src/pages/Cart.jsx b/frontend/src/pages/Cart.jsx new file mode 100644 index 0000000..c5a6b32 --- /dev/null +++ b/frontend/src/pages/Cart.jsx @@ -0,0 +1,119 @@ +import React, { useContext } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { CartContext } from '../context/CartContext' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Cart() { + const { cart, removeFromCart, updateQuantity, total, clearCart } = useContext(CartContext) + const { token } = useContext(AuthContext) + const navigate = useNavigate() + + const handleCheckout = () => { + if (!token) { + navigate('/login') + } else if (cart.length === 0) { + alert('Your cart is empty') + } else { + navigate('/checkout') + } + } + + return ( +About StyleHub
+ ++ + +Our Story
++ Founded in 2020, StyleHub started as a passion project to bring + quality fashion and footwear to customers worldwide. What began as + a small collection has grown into a comprehensive online store + featuring over 1000 products from leading brands. +
++ + +Our Mission
++ We believe everyone deserves access to stylish, high-quality + products at reasonable prices. Our mission is to make fashion + accessible, affordable, and enjoyable for everyone. +
++ + +Our Values
++
+- + Quality: We only stock products from trusted + brands and manufacturers +
+- + Customer First: Your satisfaction is our top + priority +
+- + Sustainability: We're committed to eco-friendly + practices +
+- + Innovation: We constantly improve our platform + for the best experience +
++ + +Why Shop with Us?
+++++🚚 Fast Shipping
+Orders shipped within 24 hours to most locations
+++💯 Money Back
+30-day satisfaction guarantee on all purchases
+++🔒 Secure Payment
+Your payment information is always protected
+++💬 24/7 Support
+Our team is ready to help anytime
+++🎁 Best Prices
+We offer competitive prices and frequent discounts
+++📱 Mobile App
+Shop on the go with our mobile apps
++ +Our Team
++ We have a passionate team of fashion experts, developers, and + customer service professionals working to bring you the best + shopping experience. +
+++ ) +} diff --git a/frontend/src/pages/Checkout.jsx b/frontend/src/pages/Checkout.jsx new file mode 100644 index 0000000..7817323 --- /dev/null +++ b/frontend/src/pages/Checkout.jsx @@ -0,0 +1,135 @@ +import React, { useState, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { CartContext } from '../context/CartContext' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Checkout() { + const navigate = useNavigate() + const { cart, total, clearCart } = useContext(CartContext) + const { token, user } = useContext(AuthContext) + const [loading, setLoading] = useState(false) + const [formData, setFormData] = useState({ + shipping_address: user?.address || '', + shipping_city: user?.city || '', + shipping_postal_code: user?.postal_code || '', + shipping_country: user?.country || '', + }) + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + if (!token) { + navigate('/login') + return + } + + try { + setLoading(true) + const response = await api.post('/orders', formData, { + params: { token }, + }) + + alert('Order placed successfully!') + clearCart() + navigate(`/orders/${response.data.id}`) + } catch (error) { + console.error('Error placing order:', error) + alert('Error placing order') + } finally { + setLoading(false) + } + } + + return ( +Shopping Cart
+ + {cart.length === 0 ? ( +++ ) : ( +Your cart is empty
+ + Continue Shopping + +++ )} +++ ++ +
++ + + + {cart.map((item, index) => ( +Product +Price +Quantity +Total +Action ++ + ))} + ++ ++++
++{item.product.name}
+ {item.size &&Size: {item.size}
} + {item.color &&Color: {item.color}
} +${(item.product.discount_price || item.product.price).toFixed(2)} ++ ++ + {item.quantity} + +++ ${((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)} + ++ + +++Order Summary
+++ + + + ++ Subtotal: + ${total.toFixed(2)} +++ Shipping: + $10.00 +++ Tax: + ${(total * 0.1).toFixed(2)} +++ Total: + ${(total + 10 + total * 0.1).toFixed(2)} ++++ ) +} diff --git a/frontend/src/pages/Contact.jsx b/frontend/src/pages/Contact.jsx new file mode 100644 index 0000000..e31d6be --- /dev/null +++ b/frontend/src/pages/Contact.jsx @@ -0,0 +1,143 @@ +import React, { useState } from 'react' +import api from '../api' +import '../styles/global.css' + +export default function Contact() { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }) + const [loading, setLoading] = useState(false) + const [submitted, setSubmitted] = useState(false) + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + + try { + await api.post('/contact', formData) + setSubmitted(true) + setFormData({ + name: '', + email: '', + subject: '', + message: '', + }) + + setTimeout(() => { + setSubmitted(false) + }, 3000) + } catch (error) { + console.error('Error sending message:', error) + alert('Error sending message') + } finally { + setLoading(false) + } + } + + return ( +Checkout
+ ++ + ++++Order Summary
++ {cart.map((item, index) => ( +++ {item.product.name} x{item.quantity} + ${((item.product.discount_price || item.product.price) * item.quantity).toFixed(2)} ++ ))} ++ Total: ${(total + 10 + total * 0.1).toFixed(2)} ++++ ) +} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..88bde23 --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import api from '../api' +import ProductCard from '../components/ProductCard' +import CategoryCard from '../components/CategoryCard' +import '../styles/global.css' + +export default function Home() { + const [featured, setFeatured] = useState([]) + const [newArrivals, setNewArrivals] = useState([]) + const [onSale, setOnSale] = useState([]) + const [categories, setCategories] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchData() + }, []) + + const fetchData = async () => { + try { + const [featuredRes, newRes, saleRes, catRes] = await Promise.all([ + api.get('/products?featured=true&limit=8'), + api.get('/products?limit=8'), + api.get('/products?on_sale=true&limit=8'), + api.get('/categories'), + ]) + + setFeatured(featuredRes.data) + setNewArrivals(newRes.data) + setOnSale(saleRes.data) + setCategories(catRes.data) + } catch (error) { + console.error('Error fetching data:', error) + } finally { + setLoading(false) + } + } + + if (loading) returnContact Us
+ + +Loading...+ + return ( ++ {/* Hero Section */} ++ ) +} diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..46800e0 --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,95 @@ +import React, { useState, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Login() { + const navigate = useNavigate() + const { setUser, setToken } = useContext(AuthContext) + const [formData, setFormData] = useState({ + email: '', + password: '', + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + setLoading(true) + + try { + const response = await api.post('/auth/login', null, { + params: { + email: formData.email, + password: formData.password, + }, + }) + + setToken(response.data.access_token) + setUser(response.data.user) + navigate('/') + } catch (error) { + setError('Invalid email or password') + console.error('Login error:', error) + } finally { + setLoading(false) + } + } + + return ( ++ + + {/* Category Highlights */} +++Welcome to StyleHub
+Discover the latest in fashion and footwear
+ + Shop Now + ++ + + {/* Featured Products */} +Shop by Category
++ {categories.map((cat) => ( +++ ))} + + + + {/* New Arrivals */} +Featured Products
++ {featured.map((product) => ( +++ ))} + + + + {/* On Sale */} +New Arrivals
++ {newArrivals.slice(0, 4).map((product) => ( +++ ))} + + + + {/* Best Sellers */} +🔥 Limited Time Offers
+Up to 50% off on selected items
+ + View All Sales + ++ + + {/* Promo Banner */} +Best Sellers
++ {onSale.slice(0, 4).map((product) => ( +++ ))} + + +Subscribe to Our Newsletter
+Get exclusive deals and updates
+ + +++ ) +} diff --git a/frontend/src/pages/Orders.jsx b/frontend/src/pages/Orders.jsx new file mode 100644 index 0000000..312827d --- /dev/null +++ b/frontend/src/pages/Orders.jsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Orders() { + const navigate = useNavigate() + const { token } = useContext(AuthContext) + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!token) { + navigate('/login') + } else { + fetchOrders() + } + }, [token, navigate]) + + const fetchOrders = async () => { + try { + const response = await api.get('/orders/user/orders', { + params: { token }, + }) + setOrders(response.data) + } catch (error) { + console.error('Error fetching orders:', error) + } finally { + setLoading(false) + } + } + + if (loading) return++Login
+ + {error &&{error}} + + + ++ Don't have an account? Sign up here +
+ +++Demo Account:
+Email: user@example.com
+Password: password123
+Loading...+ + return ( +++ ) +} diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx new file mode 100644 index 0000000..2e38e7c --- /dev/null +++ b/frontend/src/pages/ProductDetail.jsx @@ -0,0 +1,194 @@ +import React, { useState, useEffect, useContext } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import api from '../api' +import { CartContext } from '../context/CartContext' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function ProductDetail() { + const { id } = useParams() + const navigate = useNavigate() + const [product, setProduct] = useState(null) + const [loading, setLoading] = useState(true) + const [selectedSize, setSelectedSize] = useState('') + const [selectedColor, setSelectedColor] = useState('') + const [quantity, setQuantity] = useState(1) + const [inWishlist, setInWishlist] = useState(false) + const { addToCart } = useContext(CartContext) + const { token } = useContext(AuthContext) + + useEffect(() => { + fetchProduct() + }, [id]) + + const fetchProduct = async () => { + try { + const response = await api.get(`/products/${id}`) + setProduct(response.data) + if (response.data.colors.length > 0) setSelectedColor(response.data.colors[0]) + if (response.data.sizes.length > 0) setSelectedSize(response.data.sizes[0]) + } catch (error) { + console.error('Error fetching product:', error) + } finally { + setLoading(false) + } + } + + const handleAddToCart = async () => { + try { + if (!token) { + navigate('/login') + return + } + + await api.post('/cart/add', { + product_id: product.id, + quantity, + size: selectedSize, + color: selectedColor, + token, + }) + + addToCart(product, quantity, selectedSize, selectedColor) + alert('Product added to cart!') + } catch (error) { + console.error('Error adding to cart:', error) + } + } + + const handleToggleWishlist = async () => { + if (!token) { + navigate('/login') + return + } + + try { + if (inWishlist) { + await api.delete(`/wishlist/${product.id}`, { params: { token } }) + } else { + await api.post(`/wishlist/${product.id}`, null, { params: { token } }) + } + setInWishlist(!inWishlist) + } catch (error) { + console.error('Error updating wishlist:', error) + } + } + + if (loading) returnMy Orders
+ + {orders.length === 0 ? ( +++ ) : ( +You haven't placed any orders yet.
+ + Start Shopping + ++ {orders.map((order) => ( ++ )} +++ ))} +++ +Order #{order.order_number}
+ {order.status.toUpperCase()} +++ ++ Order Date: + {new Date(order.created_at).toLocaleDateString()} +++ Total Amount: + ${order.total_amount.toFixed(2)} +++ Items: + {order.items.length} item(s) +++ {order.items.map((item) => ( ++ +++ ))} ++
++{item.product.name}
+Qty: {item.quantity}
+${item.price.toFixed(2)}
+++Shipping Address
+{order.shipping_address}
++ {order.shipping_city}, {order.shipping_postal_code} +
+{order.shipping_country}
+Loading...+ if (!product) returnProduct not found+ + const price = product.discount_price || product.price + + return ( +++ ) +} diff --git a/frontend/src/pages/Products.jsx b/frontend/src/pages/Products.jsx new file mode 100644 index 0000000..616f478 --- /dev/null +++ b/frontend/src/pages/Products.jsx @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' +import api from '../api' +import ProductCard from '../components/ProductCard' +import ProductFilters from '../components/ProductFilters' +import '../styles/global.css' + +export default function Products() { + const [searchParams] = useSearchParams() + const [products, setProducts] = useState([]) + const [loading, setLoading] = useState(true) + const [sortBy, setSortBy] = useState('latest') + + useEffect(() => { + fetchProducts() + }, [searchParams]) + + const fetchProducts = async () => { + try { + setLoading(true) + const categorySlug = searchParams.get('category') + const params = { limit: 50 } + + if (categorySlug) { + // Get category by slug + const catRes = await api.get('/categories') + const category = catRes.data.find((c) => c.slug === categorySlug) + if (category) params.category_id = category.id + } + + const response = await api.get('/products', { params }) + let sorted = [...response.data] + + if (sortBy === 'price-low') { + sorted.sort((a, b) => (a.discount_price || a.price) - (b.discount_price || b.price)) + } else if (sortBy === 'price-high') { + sorted.sort((a, b) => (b.discount_price || b.price) - (a.discount_price || a.price)) + } else if (sortBy === 'name') { + sorted.sort((a, b) => a.name.localeCompare(b.name)) + } + + setProducts(sorted) + } catch (error) { + console.error('Error fetching products:', error) + } finally { + setLoading(false) + } + } + + const handleFilter = (filters) => { + // Apply filters locally for now + console.log('Filters:', filters) + } + + return ( +++++ ++ {product.images.slice(1).map((img, idx) => ( +
+ ))} +
++{product.name}
+{product.brand}
+ ++ ⭐⭐⭐⭐⭐ (42 reviews) ++ ++ {product.discount_price ? ( + <> + ${product.price.toFixed(2)} + ${product.discount_price.toFixed(2)} + > + ) : ( + ${price.toFixed(2)} + )} ++ +{product.description}
+ + {product.colors.length > 0 && ( ++ ++ )} + + {product.sizes.length > 0 && ( ++ {product.colors.map((color) => ( + + ))} +++ ++ )} + ++ {product.sizes.map((size) => ( + + ))} +++ ++ ++ + + +++ {product.stock > 0 ? ( + ✓ In Stock ({product.stock} available) + ) : ( + ✗ Out of Stock + )} ++ ++ + ++ +++Product Details
++
+- Category: Shoes
+- Gender: {product.gender}
+- Brand: {product.brand}
+- SKU: {product.id}
+++ ) +} diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx new file mode 100644 index 0000000..778b642 --- /dev/null +++ b/frontend/src/pages/Profile.jsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Profile() { + const navigate = useNavigate() + const { token, user, setUser } = useContext(AuthContext) + const [formData, setFormData] = useState({ + full_name: '', + phone: '', + address: '', + city: '', + postal_code: '', + country: '', + }) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!token) { + navigate('/login') + } else { + fetchProfile() + } + }, [token, navigate]) + + const fetchProfile = async () => { + try { + const response = await api.get('/users/me', { + params: { token }, + }) + setFormData(response.data) + setUser(response.data) + } catch (error) { + console.error('Error fetching profile:', error) + } finally { + setLoading(false) + } + } + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + setSaving(true) + + try { + const response = await api.put('/users/me', formData, { + params: { token }, + }) + setUser(response.data) + alert('Profile updated successfully!') + } catch (error) { + console.error('Error updating profile:', error) + alert('Error updating profile') + } finally { + setSaving(false) + } + } + + if (loading) returnOur Products
+ ++++ + +++ + + {products.length} products ++ + {loading ? ( +Loading products...+ ) : products.length > 0 ? ( ++ {products.map((product) => ( ++ ) : ( ++ ))} + ++ )} +No products found.
+Loading...+ + return ( +++ ) +} diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..b967ce7 --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,110 @@ +import React, { useState, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import '../styles/global.css' + +export default function Register() { + const navigate = useNavigate() + const { setUser, setToken } = useContext(AuthContext) + const [formData, setFormData] = useState({ + email: '', + password: '', + full_name: '', + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + setLoading(true) + + try { + const response = await api.post('/auth/register', { + email: formData.email, + password: formData.password, + full_name: formData.full_name, + }) + + alert('Account created successfully! Logging you in...') + + const loginResponse = await api.post('/auth/login', null, { + params: { + email: formData.email, + password: formData.password, + }, + }) + + setToken(loginResponse.data.access_token) + setUser(loginResponse.data.user) + navigate('/') + } catch (error) { + setError(error.response?.data?.detail || 'Registration failed') + console.error('Registration error:', error) + } finally { + setLoading(false) + } + } + + return ( +My Profile
+ ++ +++ ++ ) +} diff --git a/frontend/src/pages/Sales.jsx b/frontend/src/pages/Sales.jsx new file mode 100644 index 0000000..1d1095d --- /dev/null +++ b/frontend/src/pages/Sales.jsx @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react' +import api from '../api' +import ProductCard from '../components/ProductCard' +import '../styles/global.css' + +export default function Sales() { + const [products, setProducts] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchSaleProducts() + }, []) + + const fetchSaleProducts = async () => { + try { + const response = await api.get('/products', { + params: { + on_sale: true, + limit: 50, + }, + }) + setProducts(response.data) + } catch (error) { + console.error('Error fetching sale products:', error) + } finally { + setLoading(false) + } + } + + return ( +++ ) +} diff --git a/frontend/src/pages/Wishlist.jsx b/frontend/src/pages/Wishlist.jsx new file mode 100644 index 0000000..5122744 --- /dev/null +++ b/frontend/src/pages/Wishlist.jsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect, useContext } from 'react' +import { useNavigate } from 'react-router-dom' +import api from '../api' +import { AuthContext } from '../context/AuthContext' +import ProductCard from '../components/ProductCard' +import '../styles/global.css' + +export default function Wishlist() { + const navigate = useNavigate() + const { token } = useContext(AuthContext) + const [wishlistItems, setWishlistItems] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!token) { + navigate('/login') + } else { + fetchWishlist() + } + }, [token, navigate]) + + const fetchWishlist = async () => { + try { + const response = await api.get('/wishlist', { + params: { token }, + }) + setWishlistItems(response.data) + } catch (error) { + console.error('Error fetching wishlist:', error) + } finally { + setLoading(false) + } + } + + const handleRemoveFromWishlist = async (productId) => { + try { + await api.delete(`/wishlist/${productId}`, { + params: { token }, + }) + setWishlistItems( + wishlistItems.filter((item) => item.id !== productId) + ) + } catch (error) { + console.error('Error removing from wishlist:', error) + } + } + + if (loading) return++ + {loading ? ( +🔥 Limited Time Offers
+Huge discounts on selected items - Limited time only!
+Loading...+ ) : products.length > 0 ? ( +++ ) : ( ++ {products.map((product) => ( ++++ ))} ++ ++ )} +No products on sale at the moment.
+Loading...+ + return ( +++ ) +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css new file mode 100644 index 0000000..ddbeea9 --- /dev/null +++ b/frontend/src/styles/global.css @@ -0,0 +1,1477 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #ff6b6b; + --primary-dark: #ff5252; + --secondary: #1a1a1a; + --light: #f5f5f5; + --gray: #888888; + --gray-light: #e0e0e0; + --border-radius: 8px; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + color: var(--secondary); + line-height: 1.6; + background-color: #fff; +} + +html, body, #root { + height: 100%; +} + +a { + text-decoration: none; + color: inherit; +} + +button { + cursor: pointer; + font-family: inherit; + border: none; + border-radius: var(--border-radius); +} + +/* App Layout */ +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.main-content { + flex: 1; + padding: 0; +} + +/* Navbar */ +.navbar { + background: white; + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; +} + +.navbar-container { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; +} + +.navbar-logo { + font-size: 1.5rem; + font-weight: bold; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.logo-icon { + font-size: 2rem; +} + +.navbar-center { + flex: 1; + max-width: 400px; +} + +.navbar-menu { + display: flex; + gap: 2rem; + list-style: none; + flex-wrap: wrap; +} + +.navbar-menu a { + color: var(--secondary); + font-weight: 500; + transition: color 0.3s; +} + +.navbar-menu a:hover { + color: var(--primary); +} + +.navbar-icons { + display: flex; + align-items: center; + gap: 1rem; +} + +.icon-btn { + font-size: 1.5rem; + cursor: pointer; + transition: transform 0.3s; + position: relative; +} + +.icon-btn:hover { + transform: scale(1.1); +} + +.cart-count { + position: absolute; + top: -8px; + right: -8px; + background: var(--primary); + color: white; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + background: var(--primary); + color: white; + font-weight: 600; + transition: background 0.3s; + display: inline-block; + text-align: center; + white-space: nowrap; +} + +.btn:hover:not(:disabled) { + background: var(--primary-dark); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-small { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1rem; +} + +.btn-full { + width: 100%; +} + +.btn-secondary { + background: transparent; + color: var(--primary); + border: 2px solid var(--primary); +} + +.btn-secondary:hover { + background: var(--light); +} + +.btn-outline { + background: transparent; + color: var(--secondary); + border: 2px solid var(--gray-light); +} + +.btn-outline:hover { + border-color: var(--secondary); +} + +.btn-danger { + background: #e74c3c; +} + +.btn-danger:hover { + background: #c0392b; +} + +.btn-icon { + padding: 0.5rem; + background: transparent; + font-size: 1.5rem; + border-radius: 50%; + transition: background 0.3s; +} + +.btn-icon:hover { + background: var(--light); +} + +.btn-icon.active { + background: var(--primary) + '20'; +} + +/* Grid */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +/* Sections */ +.section { + max-width: 1200px; + margin: 4rem auto; + padding: 0 2rem; +} + +.section h2 { + font-size: 2rem; + margin-bottom: 1rem; + color: var(--secondary); +} + +/* Hero Section */ +.hero { + background: linear-gradient(135deg, var(--primary) 0%, #ff8787 100%); + color: white; + padding: 6rem 2rem; + text-align: center; +} + +.hero-content h1 { + font-size: 3rem; + margin-bottom: 1rem; +} + +.hero-content p { + font-size: 1.2rem; + margin-bottom: 2rem; +} + +/* Product Card */ +.product-card { + background: white; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow); + transition: transform 0.3s, box-shadow 0.3s; +} + +.product-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.product-image-container { + position: relative; + overflow: hidden; + aspect-ratio: 1; + background: var(--light); +} + +.product-image-container img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; +} + +.product-card:hover .product-image-container img { + transform: scale(1.05); +} + +.discount-badge, .featured-badge { + position: absolute; + top: 10px; + right: 10px; + background: var(--primary); + color: white; + padding: 0.5rem 1rem; + border-radius: 4px; + font-weight: bold; + font-size: 0.875rem; +} + +.featured-badge { + right: auto; + left: 10px; + background: #4CAF50; +} + +.product-info { + padding: 1.5rem; +} + +.product-info h3 { + font-size: 1.1rem; + margin-bottom: 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.brand { + color: var(--gray); + font-size: 0.875rem; + margin-bottom: 0.75rem; +} + +.price { + margin-bottom: 0.75rem; + display: flex; + gap: 0.5rem; + align-items: center; +} + +.price .original { + color: var(--gray); + text-decoration: line-through; + font-size: 0.9rem; +} + +.price .discounted, +.price .current { + font-weight: bold; + font-size: 1.2rem; + color: var(--primary); +} + +.stock { + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.in-stock { + color: #4CAF50; + font-weight: 600; +} + +.out-of-stock { + color: #e74c3c; + font-weight: 600; +} + +/* Category Card */ +.category-card { + display: block; + background: white; + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--shadow); + transition: transform 0.3s; + cursor: pointer; +} + +.category-card:hover { + transform: translateY(-4px); +} + +.category-card img { + width: 100%; + height: 200px; + object-fit: cover; +} + +.category-card h3 { + padding: 1rem; + font-size: 1.2rem; +} + +.category-card p { + padding: 0 1rem 1rem; + color: var(--gray); + font-size: 0.875rem; +} + +/* Search Bar */ +.search-bar { + position: relative; + width: 100%; +} + +.search-bar input { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--gray-light); + border-radius: var(--border-radius); + font-size: 0.95rem; +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid var(--gray-light); + border-top: none; + border-radius: 0 0 var(--border-radius) var(--border-radius); + max-height: 300px; + overflow-y: auto; + z-index: 10; +} + +.search-result-item { + display: block; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--gray-light); + display: flex; + justify-content: space-between; + cursor: pointer; + transition: background 0.2s; +} + +.search-result-item:hover { + background: var(--light); +} + +/* Filters */ +.filters-sidebar { + background: var(--light); + padding: 1.5rem; + border-radius: var(--border-radius); + height: fit-content; +} + +.filters-sidebar h3 { + margin-bottom: 1.5rem; + font-size: 1.1rem; +} + +.filter-group { + margin-bottom: 1.5rem; +} + +.filter-group label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; + font-size: 0.9rem; +} + +.filter-group select, +.filter-group input[type="checkbox"] { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--gray-light); + border-radius: 4px; + font-size: 0.9rem; +} + +.filter-group input[type="checkbox"] { + width: auto; + margin-right: 0.5rem; +} + +/* Products Page */ +.products-page { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.products-page h1 { + margin-bottom: 2rem; + font-size: 2rem; +} + +.products-container { + display: grid; + grid-template-columns: 250px 1fr; + gap: 2rem; +} + +.products-content { + display: flex; + flex-direction: column; +} + +.sort-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + gap: 1rem; + flex-wrap: wrap; +} + +.sort-bar select { + padding: 0.5rem 1rem; + border: 1px solid var(--gray-light); + border-radius: var(--border-radius); +} + +.product-count { + color: var(--gray); + font-size: 0.9rem; +} + +.no-products { + text-align: center; + padding: 3rem 1rem; + color: var(--gray); +} + +/* Product Detail */ +.product-detail { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.detail-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + margin-top: 2rem; +} + +.detail-images { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.detail-images img { + width: 100%; + max-height: 500px; + object-fit: cover; + border-radius: var(--border-radius); + cursor: pointer; +} + +.detail-info h1 { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.detail-info .brand { + font-size: 1rem; + margin-bottom: 1rem; +} + +.rating { + margin-bottom: 1.5rem; + font-size: 1rem; +} + +.detail-info .description { + color: var(--gray); + line-height: 1.8; + margin-bottom: 1.5rem; +} + +.option-group { + margin-bottom: 1.5rem; +} + +.option-group label { + display: block; + font-weight: 600; + margin-bottom: 0.75rem; +} + +.color-options, +.size-options { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.color-btn, +.size-btn { + padding: 0.75rem 1rem; + border: 2px solid var(--gray-light); + border-radius: var(--border-radius); + background: white; + cursor: pointer; + transition: all 0.3s; +} + +.color-btn:hover, +.size-btn:hover { + border-color: var(--primary); +} + +.color-btn.active, +.size-btn.active { + border-color: var(--primary); + background: var(--primary); + color: white; +} + +.quantity-selector { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid var(--gray-light); + border-radius: var(--border-radius); + width: fit-content; +} + +.quantity-selector button { + width: 40px; + height: 40px; + background: transparent; + cursor: pointer; + font-size: 1.2rem; +} + +.quantity-selector input { + width: 60px; + border: none; + text-align: center; + font-weight: bold; +} + +.stock-info { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--light); + border-radius: var(--border-radius); +} + +.action-buttons { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.action-buttons .btn { + flex: 1; +} + +.action-buttons .btn-icon { + flex: 0; + width: auto; +} + +.product-details { + padding-top: 2rem; + border-top: 1px solid var(--gray-light); +} + +.product-details h3 { + margin-bottom: 1rem; +} + +.product-details ul { + list-style: none; +} + +.product-details li { + padding: 0.5rem 0; + color: var(--gray); +} + +/* Cart Page */ +.cart-page { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.cart-page h1 { + margin-bottom: 2rem; +} + +.empty-cart { + text-align: center; + padding: 3rem; + background: var(--light); + border-radius: var(--border-radius); +} + +.empty-cart p { + margin-bottom: 1.5rem; + color: var(--gray); +} + +.cart-container { + display: grid; + grid-template-columns: 1fr 350px; + gap: 2rem; +} + +.cart-table { + width: 100%; + border-collapse: collapse; +} + +.cart-table th { + background: var(--light); + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 2px solid var(--gray-light); +} + +.cart-table td { + padding: 1rem; + border-bottom: 1px solid var(--gray-light); +} + +.cart-item-product { + display: flex; + gap: 1rem; + align-items: center; +} + +.cart-item-product img { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; +} + +.product-name { + font-weight: 600; +} + +.quantity-control { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid var(--gray-light); + border-radius: 4px; + width: fit-content; +} + +.quantity-control button { + width: 30px; + height: 30px; + background: transparent; + cursor: pointer; +} + +.cart-summary { + background: var(--light); + padding: 1.5rem; + border-radius: var(--border-radius); + height: fit-content; + position: sticky; + top: 100px; +} + +.cart-summary h3 { + margin-bottom: 1.5rem; +} + +.summary-rows { + margin-bottom: 1.5rem; +} + +.summary-row { + display: flex; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid var(--gray-light); +} + +.summary-row.total { + font-weight: bold; + font-size: 1.1rem; + border-top: 2px solid var(--gray-light); + border-bottom: none; + padding: 1rem 0; +} + +/* Checkout Page */ +.checkout-page { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.checkout-page h1 { + margin-bottom: 2rem; +} + +.checkout-container { + display: grid; + grid-template-columns: 1fr 350px; + gap: 2rem; +} + +.checkout-form, +.checkout-summary { + background: var(--light); + padding: 2rem; + border-radius: var(--border-radius); +} + +.form-section { + margin-bottom: 2rem; +} + +.form-section h2 { + font-size: 1.3rem; + margin-bottom: 1rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--gray-light); + border-radius: var(--border-radius); + font-size: 0.95rem; + font-family: inherit; +} + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.checkout-summary { + height: fit-content; + position: sticky; + top: 100px; +} + +.summary-items { + margin-bottom: 1.5rem; +} + +.summary-item { + display: flex; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid var(--gray-light); + font-size: 0.9rem; +} + +.summary-total { + padding-top: 1rem; + font-size: 1.1rem; + color: var(--primary); +} + +/* Auth Pages */ +.auth-page { + display: flex; + justify-content: center; + align-items: center; + min-height: calc(100vh - 200px); + padding: 2rem; +} + +.auth-container { + background: white; + padding: 3rem; + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 400px; +} + +.auth-container h1 { + margin-bottom: 2rem; + text-align: center; +} + +.auth-container form { + margin-bottom: 1.5rem; +} + +.auth-container p { + text-align: center; + color: var(--gray); + margin-bottom: 1rem; +} + +.auth-container a { + color: var(--primary); + font-weight: 600; +} + +.error-message { + background: #f8d7da; + color: #721c24; + padding: 1rem; + border-radius: var(--border-radius); + margin-bottom: 1rem; + text-align: center; +} + +.demo-account { + background: #e3f2fd; + padding: 1rem; + border-radius: var(--border-radius); + margin-top: 1.5rem; + font-size: 0.875rem; +} + +.demo-account p { + margin: 0.25rem 0; + text-align: left; +} + +/* Profile Page */ +.profile-page, +.orders-page, +.wishlist-page { + max-width: 1000px; + margin: 0 auto; + padding: 2rem; +} + +.profile-page h1, +.orders-page h1, +.wishlist-page h1 { + margin-bottom: 2rem; +} + +.profile-container { + background: var(--light); + padding: 2rem; + border-radius: var(--border-radius); +} + +.profile-form h2 { + font-size: 1.3rem; + margin: 1.5rem 0 1rem; +} + +.profile-form h2:first-child { + margin-top: 0; +} + +/* Orders Page */ +.orders-container { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.order-card { + background: white; + border: 1px solid var(--gray-light); + border-radius: var(--border-radius); + padding: 2rem; + box-shadow: var(--shadow); +} + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--gray-light); + padding-bottom: 1rem; +} + +.order-header h3 { + font-size: 1.2rem; +} + +.status { + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; +} + +.status.pending { + background: #fff3cd; + color: #856404; +} + +.status.paid { + background: #d4edda; + color: #155724; +} + +.status.shipped { + background: #d1ecf1; + color: #0c5460; +} + +.status.delivered { + background: #d4edda; + color: #155724; +} + +.order-details { + margin-bottom: 1.5rem; +} + +.detail-row { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + color: var(--gray); +} + +.order-items { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--gray-light); +} + +.order-item { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 0.75rem; +} + +.order-item img { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 4px; +} + +.shipping-info { + background: var(--light); + padding: 1rem; + border-radius: var(--border-radius); +} + +.shipping-info h4 { + margin-bottom: 0.5rem; +} + +.shipping-info p { + font-size: 0.9rem; + color: var(--gray); + margin-bottom: 0.25rem; +} + +/* Wishlist Page */ +.wishlist-item { + position: relative; +} + +.wishlist-item .btn { + margin-top: 0.5rem; +} + +/* About Page */ +.about-page { + max-width: 1000px; + margin: 0 auto; + padding: 2rem; +} + +.about-page h1 { + margin-bottom: 2rem; + font-size: 2.5rem; +} + +.about-section { + margin-bottom: 3rem; +} + +.about-section h2 { + font-size: 1.8rem; + margin-bottom: 1rem; + color: var(--primary); +} + +.about-section p { + font-size: 1rem; + line-height: 1.8; + color: var(--gray); +} + +.values-list { + list-style: none; + margin-top: 1rem; +} + +.values-list li { + padding: 1rem; + margin-bottom: 0.75rem; + background: var(--light); + border-left: 4px solid var(--primary); + border-radius: 4px; +} + +.why-us-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.why-us-card { + background: var(--light); + padding: 1.5rem; + border-radius: var(--border-radius); + text-align: center; + transition: transform 0.3s; +} + +.why-us-card:hover { + transform: translateY(-4px); +} + +.why-us-card h3 { + font-size: 1.2rem; + margin-bottom: 0.75rem; +} + +/* Contact Page */ +.contact-page { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.contact-page h1 { + margin-bottom: 2rem; +} + +.contact-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + margin-top: 2rem; +} + +.contact-info h2 { + font-size: 1.5rem; + margin-bottom: 2rem; +} + +.info-item { + margin-bottom: 2rem; +} + +.info-item h3 { + font-size: 1.1rem; + margin-bottom: 0.75rem; + color: var(--primary); +} + +.info-item p { + color: var(--gray); + line-height: 1.8; +} + +.social-links { + display: flex; + gap: 1rem; +} + +.social-links a { + display: inline-block; + width: 40px; + height: 40px; + background: var(--primary); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.3s; +} + +.social-links a:hover { + background: var(--primary-dark); +} + +.contact-form { + background: var(--light); + padding: 2rem; + border-radius: var(--border-radius); +} + +.contact-form h2 { + font-size: 1.5rem; + margin-bottom: 1.5rem; +} + +.success-message { + background: #d4edda; + color: #155724; + padding: 1rem; + border-radius: var(--border-radius); + margin-bottom: 1rem; + text-align: center; + font-weight: 600; +} + +/* Sales Page */ +.sales-page { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.sales-header { + background: linear-gradient(135deg, var(--primary) 0%, #ff8787 100%); + color: white; + padding: 3rem 2rem; + border-radius: var(--border-radius); + text-align: center; + margin-bottom: 2rem; +} + +.sales-header h1 { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.sales-header p { + font-size: 1.1rem; +} + +/* Sale Banner */ +.sale-banner { + background: linear-gradient(135deg, var(--primary) 0%, #ff8787 100%); + color: white; + padding: 3rem 2rem; + text-align: center; + border-radius: var(--border-radius); +} + +.sale-banner h2 { + color: white; +} + +.sale-banner p { + font-size: 1.1rem; + margin-bottom: 1.5rem; +} + +/* Promo Banner */ +.promo-banner { + background: linear-gradient(135deg, var(--secondary) 0%, #424242 100%); + color: white; + padding: 3rem 2rem; + text-align: center; + border-radius: var(--border-radius); + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.promo-banner h3 { + font-size: 1.5rem; +} + +.promo-banner input { + max-width: 400px; + width: 100%; + padding: 0.75rem 1rem; + border: none; + border-radius: var(--border-radius); +} + +.promo-banner .btn { + max-width: 400px; +} + +/* Footer */ +.footer { + background: var(--secondary); + color: white; + margin-top: auto; + padding: 3rem 0 1rem; +} + +.footer-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; +} + +.footer-section h3, +.footer-section h4 { + margin-bottom: 1rem; + font-size: 1rem; +} + +.footer-section p { + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + line-height: 1.8; +} + +.footer-section a { + display: block; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 0.5rem; + transition: color 0.3s; +} + +.footer-section a:hover { + color: white; +} + +.social-links { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.footer-bottom { + text-align: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; +} + +/* Loading & Empty States */ +.loading, +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--gray); + font-size: 1.1rem; +} + +.empty-state { + background: var(--light); + border-radius: var(--border-radius); + margin: 2rem auto; + max-width: 500px; +} + +.empty-state .btn { + margin-top: 1.5rem; +} + +.text-muted { + color: var(--gray); + font-size: 0.9rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .navbar-container { + flex-wrap: wrap; + gap: 1rem; + } + + .navbar-center { + order: 3; + flex-basis: 100%; + max-width: 100%; + } + + .navbar-menu { + gap: 1rem; + font-size: 0.9rem; + } + + .grid { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1rem; + } + + .hero-content h1 { + font-size: 2rem; + } + + .detail-container { + grid-template-columns: 1fr; + } + + .products-container { + grid-template-columns: 1fr; + } + + .filters-sidebar { + display: none; + } + + .cart-container, + .checkout-container, + .contact-container { + grid-template-columns: 1fr; + } + + .cart-summary, + .checkout-summary { + position: static; + } + + .cart-table { + font-size: 0.85rem; + } + + .cart-table th, + .cart-table td { + padding: 0.5rem; + } + + .sort-bar { + flex-direction: column; + align-items: flex-start; + } + + .footer-container { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .navbar-logo { + font-size: 1.2rem; + } + + .navbar-icons { + gap: 0.5rem; + } + + .btn { + padding: 0.6rem 1rem; + font-size: 0.9rem; + } + + .grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.75rem; + } + + .hero-content h1 { + font-size: 1.5rem; + } + + .section { + padding: 0 1rem; + } + + .auth-container { + padding: 1.5rem; + } + + .contact-form, + .profile-container { + padding: 1rem; + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..1406734 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +})My Wishlist
+ + {wishlistItems.length === 0 ? ( +++ ) : ( +Your wishlist is empty
+ + Browse Products + +++ )} ++ {wishlistItems.map((product) => ( ++++ ))} ++ +