From 14fffa40c683c3727295f3605bd74617e75e5854 Mon Sep 17 00:00:00 2001 From: Anbarasu22004 Date: Thu, 23 Jul 2026 15:36:11 +0530 Subject: [PATCH] Update project --- android/app/build.gradle.kts | 5 +- android/app/proguard-rules.pro | 15 + android/app/src/main/AndroidManifest.xml | 43 +- .../example/nearlev1/HomeWidgetProvider.kt | 28 + .../com/example/nearlev1/MainActivity.kt | 37 +- .../main/res/layout/home_widget_layout.xml | 12 + .../app/src/main/res/xml/home_widget_info.xml | 8 + android/app/src/main/res/xml/shortcuts.xml | 10 + .../reports/problems/problems-report.html | 2 +- android/gradle.properties | 4 + android/settings.gradle.kts | 4 +- assets/images/dailyLogo.png | Bin 12208 -> 0 bytes assets/images/nearledash3.png | Bin 94484 -> 0 bytes assets/lotties/ai.json | 1 + assets/lotties/ai_loader.json | 1 + assets/lotties/voice.json | 1 + ios/Podfile | 2 +- ios/Podfile.lock | 179 +- ios/Runner.xcodeproj/project.pbxproj | 128 +- .../xcshareddata/swiftpm/Package.resolved | 9 + .../xcshareddata/swiftpm/Package.resolved | 9 + ios/Runner/Info.plist | 23 +- .../authentication/auth_controller.dart | 17 +- lib/controllers/cart_controller/cart.dart | 5 +- .../dashboard_controller.dart | 2 +- .../create_order_controller.dart | 10 +- .../GrocerySearchController.dart | 27 + .../search_controller/product_match.dart | 485 ++++ .../search_controller/search_controller.dart | 295 +++ .../tenant_controller.dart} | 0 lib/core/services/ExitWrapper.dart | 0 lib/core/services/vector_service.dart | 86 + .../authentication/auth_provider.dart | 2 + lib/features/Products/product_card.dart | 316 +++ .../controller/store_controller.dart | 120 + .../presentation/screens/store_view.dart | 454 ++++ .../presentation/widgets/store_widgets.dart | 425 ++++ .../category/prestation/screens/category.dart | 1934 +++++++++++++++ .../dashboard/data/grocery_product_model.dart | 243 ++ .../domain/repo/grocery_repository.dart | 89 + .../screens/ai_processing_screen.dart | 1424 +++++++++++ .../presentation/screens/camera_search.dart | 526 ++++ .../presentation/screens/daily_plan.dart | 1690 +++++++++++++ .../presentation/screens/dash_view.dart | 1551 ++++++++++++ .../presentation/screens/edit_object.dart | 597 +++++ .../screens/voice_assistant_screen.dart | 334 +++ .../orders/presentation/order_tracking.dart | 744 ++++++ lib/main.dart | 390 ++- lib/modules/search/grocery_item.dart | 24 + lib/modules/search_model/search_model.dart | 69 + lib/service/ai_service/api_service.dart | 58 + lib/service/ai_service/product_service.dart | 0 lib/service/ai_service/store_service.dart | 0 .../firebase_analytics/analytics_service.dart | 28 + lib/view/account/account_view.dart | 40 +- lib/view/account/share_app.dart | 775 +++--- .../authentication/costomer_create_view.dart | 29 +- lib/view/authentication/login_view.dart | 10 +- lib/view/cart/cart_view.dart | 28 +- lib/view/cart/order_countdown_page.dart | 14 + lib/view/dashboard_view/ai_search.dart | 1625 +++++++++++++ lib/view/dashboard_view/dashboard_view.dart | 574 +++-- lib/view/dashboard_view/searchScreen.dart | 967 ++++---- lib/view/dashboard_view/tenant_profile.dart | 213 +- lib/view/dashboard_view/test.dart | 560 +++++ lib/view/home_view.dart | 46 +- lib/view/orders/order_succes.dart | 38 + lib/view/orders/orders_by_tenant.dart | 240 +- lib/view/product/product_details.dart | 886 +++++++ lib/view/product/product_view.dart | 3 - lib/view/product/products_list.dart | 878 +++++++ lib/view/product/refill_home_screen.dart | 1228 ++++++++++ lib/view/product/tenant_products.dart | 2144 +++++++++-------- lib/view/qr_scaner/qr_scaner.dart | 2 +- lib/widgets/fav_button.dart | 118 + lib/widgets/grocery_card.dart | 107 + lib/widgets/search_widgets.dart | 383 +++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.lock | 262 +- pubspec.yaml | 24 +- .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 84 files changed, 20918 insertions(+), 2761 deletions(-) create mode 100644 android/app/src/main/kotlin/com/example/nearlev1/HomeWidgetProvider.kt create mode 100644 android/app/src/main/res/layout/home_widget_layout.xml create mode 100644 android/app/src/main/res/xml/home_widget_info.xml create mode 100644 android/app/src/main/res/xml/shortcuts.xml delete mode 100644 assets/images/dailyLogo.png delete mode 100644 assets/images/nearledash3.png create mode 100644 assets/lotties/ai.json create mode 100644 assets/lotties/ai_loader.json create mode 100644 assets/lotties/voice.json create mode 100644 lib/controllers/search_controller/GrocerySearchController.dart create mode 100644 lib/controllers/search_controller/product_match.dart create mode 100644 lib/controllers/search_controller/search_controller.dart rename lib/controllers/{tenant_controller /tenant_list.dart => tenant_list/tenant_controller.dart} (100%) create mode 100644 lib/core/services/ExitWrapper.dart create mode 100644 lib/core/services/vector_service.dart create mode 100644 lib/features/Products/product_card.dart create mode 100644 lib/features/Store/presentation/controller/store_controller.dart create mode 100644 lib/features/Store/presentation/screens/store_view.dart create mode 100644 lib/features/Store/presentation/widgets/store_widgets.dart create mode 100644 lib/features/category/prestation/screens/category.dart create mode 100644 lib/features/dashboard/data/grocery_product_model.dart create mode 100644 lib/features/dashboard/domain/repo/grocery_repository.dart create mode 100644 lib/features/dashboard/presentation/screens/ai_processing_screen.dart create mode 100644 lib/features/dashboard/presentation/screens/camera_search.dart create mode 100644 lib/features/dashboard/presentation/screens/daily_plan.dart create mode 100644 lib/features/dashboard/presentation/screens/dash_view.dart create mode 100644 lib/features/dashboard/presentation/screens/edit_object.dart create mode 100644 lib/features/dashboard/presentation/screens/voice_assistant_screen.dart create mode 100644 lib/features/orders/presentation/order_tracking.dart create mode 100644 lib/modules/search/grocery_item.dart create mode 100644 lib/modules/search_model/search_model.dart create mode 100644 lib/service/ai_service/api_service.dart create mode 100644 lib/service/ai_service/product_service.dart create mode 100644 lib/service/ai_service/store_service.dart create mode 100644 lib/service/firebase_analytics/analytics_service.dart create mode 100644 lib/view/dashboard_view/ai_search.dart create mode 100644 lib/view/dashboard_view/test.dart create mode 100644 lib/view/product/product_details.dart create mode 100644 lib/view/product/products_list.dart create mode 100644 lib/view/product/refill_home_screen.dart create mode 100644 lib/widgets/fav_button.dart create mode 100644 lib/widgets/grocery_card.dart create mode 100644 lib/widgets/search_widgets.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 01557e3..bc4f4a7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -18,7 +18,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.nearle.gear" compileSdk = 36 - ndkVersion = "27.0.12077973" + ndkVersion = "28.2.13676358" defaultConfig { applicationId = "com.nearle.gear" @@ -59,6 +59,9 @@ android { )} } } +configurations.all { + exclude(group = "com.google.firebase", module = "firebase-iid") +} dependencies { diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index f1f8993..53acfce 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -53,3 +53,18 @@ # Keep annotations ############################### -keepattributes *Annotation* + +############################### +# Firebase InstanceID (legacy, referenced by mlkit-linkfirebase only) +############################### +-dontwarn com.google.firebase.iid.** + +############################### +# ML Kit text recognition — language packs not bundled +############################### +-dontwarn com.google.mlkit.vision.text.chinese.** +-dontwarn com.google.mlkit.vision.text.devanagari.** +-dontwarn com.google.mlkit.vision.text.japanese.** +-dontwarn com.google.mlkit.vision.text.korean.** +-dontwarn com.google.mlkit.linkfirebase.** +-dontwarn com.google.mlkit.** \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 261aa0d..da2efbb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,20 +2,17 @@ - - - - + + + + @@ -63,6 +63,7 @@ + @@ -74,16 +75,50 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dyt5(Gd-KPz2(Je5Gx zu1h9q+ab^BDO_fQ6Au5XPLFZ61@zY0YClWZxr^xxPX>04SwO?wsrQyzPc?tjqg9O5 z_6ZKAeQN?4vH1fw)+CIZ{Q2%Jr6dTOkF&5RPQPaRG@4yki43Z>Al42N`v#bFxW|Fa zvZkLG)>tL#vK5@QdPotoOk5u)lE&s_G1XRl9ON|+`EnE4BkkGgQs>_3GN&Iz-2oua zL}L#ka()u!)wgMRAsveY)m+;&zKhk_%z<}aW7>{`+{MeVD;Jrq7wLxOH^fa9yDI0A z*{MXs*}`aSgxHU5s(JVygXU7gxz#l0z`BJDZO-7cZY7X^dXIlFFOISC4GASg-kfDU zqQJq{WuuXJPSDo+9&%Xw##VJCxpGJVQr+blqfEz{c*`d*(j9$QRQz|}QTMNOy;Bt` zSBQ1WLVWcaEdY>gB`fMGMTGluC^LVQ@%fX9;$}aI-yw0Mkc)Xq6HSjeEKhP}f)3_l zb?G1W2CyqHU6M?fTaRU(>Tb4PrgLaCU`weKQxt~Z0q;F2j-#~m>)ks3Ah?#hDET%y zkC0U0jGP;71fRz_JDmp_`K4!A@`vk9I3RyZixii$b9zDcFo5j1ajU1tHYytTS|@8N z%X4NZtHY-Z<$eAXJZEI^=N=-CsP(a9wZZDRhn6GMRP}Way9~WlhZ?slWW(2EGrw^u zbJ&$AHt+Z1tYnU?Ec#Z5i)sS%qJUg{zylcYsNchiKC}hvBs7RI`D#jp;&)#1tl46^jkx`{TY~epO;3eUdoRyrfdj zN2fVmfWq(?ku-R0+PB1@Moo2fb0_Q{bN#BV6{Bi|Bkr0yT-Edq_m)*DI=4@whguE~ z&+pc8CV96G7efUt_w35bDctmb&|Lc}tqm*UiZ|;10}2aBpz$!1N1Py1K;{nD{@3oV zBr^QBwo`MW!KGEu!YrLL^wvQ{R6SUOPO;svhi9QUomd~-?v0JM7Pz^rMd{7bRs5>P zK)lB^f>)Mss|5-l8iG%XS!wsdvjXJ4#dHxok_hZPNev7^3dg84mVO(lk|XGiQu>jl zjYC7OYS@8&wV~K;H0kWUqa4;2w}F-hn(D5jLWt>+*rs}HMSHLiqUA&umpH{;p9^cN zU3k{3w*Vr$l$d!mzB9h$N5JUP-0JoF6uh2WYQRQFIjtrPT8^ThZ0E}BhGVOX3(8Z_ zgC*D_4_En{Vx5<7V9uobA@gqbB%kA$Ep9?Qg6eFTgobGWm%wu)dkI=@N)oUI|W$j zp=vM^>m-zD>3=$c^f?-E4%)|OipUf;<4NKhqHe3Ji8em=*4>yB>nF8Djsw8;R{U9@ za3|C~s$Yy5FBQ}5G-fF{ze#_5ofLxy5Iw-6F&1uj$Un6lXMZOmz)i4Blvi33uRf$M zJnGmdvLBeFR%hn7X&`+4PP=7vU%8XOsWD7D%~Q5|c5e1ariwdmdt?(!+6n!D$1kSQ z9CrkBs`N4c5OUs^5(7dP|NJ}(^5`IG-#t#n{PV5Mo@qJ3l2lB{Nvr~w9*GPOkS+=# z){vzpUm`>gKRX$QYV`~{*ZIQI(n-#e89 zd#xOpL-h-{17Mgxx!B?vU%`on|1uS3VBr=A#}o&C{yX}E`)vTQ*y>#`l#u1 z&!^Ajf&$i2g>rQTRQ$w@tQiM_X)N$#wNj{%^4hfY=ZXy>v*i=B5#qBNP@)tl3oGp3 zmHFP}kb)lp0TV%+B!_HGR>XkACKpitZP8B~#+Q19^TN(q`15iZGH!kwpsT?&F{@Jb zsRBQui-kqgG-xl#aYP|V1p7*e4NFt$UBrxxEHx@}px0%MMc~hcT;xBiwjbs&VrO^t zJOv=g!TciqoMw=~Nb6s^Z^_Qf45W=xVN1J9Jnxd?AB8mwOl>k)Rn2F+FXZbpecI$Z zYKV?WAHu&4F?R<#-Lmp@qTsfXA){7if6lM<6cAu zN+VtLw~6$5$U#6Fda-)Qq^UOO3^5K?y+cAckVMiq;%j_s^{Q4c;#<4i$m{)CZ>lRoH8&P@nt>IJ@*ELizL^=X-f%Xtp+LUmc$an4_~(Q`DfIK`G0B! z1ZFDcq1+*F!X8N@A;V&3WFa0|Vi@BYhU%|65sHwxXH%S!o9Vu9B!NoTw?q@rdRrQT z?-SUE7T9;-S^d)#<8ufu6sk-5gM&A(%u<&>x^XsX3qT&y3N#v&Ml6$V}jc3$O(Uw>9eY)A5hbf))V)dW8Z(&6R>pIe=x%?AU2_k*FT2^4YrN#ujFHUp#KJ(QkM5hNfua9>2=r zpDB95S3P>LmwC$}VwmjB3TrT>&uOj^y4|%KF(>VuWZm4(bvYOy@pMrFZmeF}DOW)? zU{uKH`hXL^7l<^)sNTr=1ud)#T_7EKR^6JmuU)y|;8v)+L%Dw_X5O9-rkl2QOr?@a zuBjza0_vfNrfw*V3Ea+ikWQ^V_ScZgtly(~ayXD7z_Q*#|VMW?_{#{f=?x#Lp?V~_PAdEeM z`*sG;MGF=Osg6s&Dw)zqY1KSP3;GYm@u9oOk8aanHsE&M$o=9edp`}IhdljYDCtjQ z&x|U_7dv;jIMc%6r3B(841;7k=G18OAG`NG1Z$l1);G+-PWXfaXbMpaRGP^rZHBjF~>4-|Sb-2(gym&rha0Zg*<0mbXI) zZQ;Lr|0K$Y|5>c}5ktloT_|_qPcAHt-fSo`t?!3q*07+)_2_Hi?Z(9UN6!tVpP&z*FqCSuDgQEE#ZX=Q?HFXK@dfqYDLUzJ$|KNiP_6mXi-v9Pr z-;ayXitYBl%{o{Y5e01f`jvfE9+G6~Y$*s{N>7nJRabHWALUU?{qg9yEAu^onA5f_ zE?#?>KY|r>Yugj$A1$wWn8vUKJD0_vV!vWrYo-kg=wrDtz76yR@o`IA1;9vdSx1tB z&f=Z+ochvSD)<9SGhWntItW7IIpxaKCJ}sZtU&k4eD;8H_PML$9oOFpo$vD}Oaec} z9T!QYZ_c3XsNBnkEx?XpmUsaS%ZG8vPN>FF)poT>ZMKT71*CyW>jWNzHB zW~Kls{=+;hLP!s;34p;4$HtGEuV&9{CEKu|oWFQjz&X84X;RbP=HpM6$yvQ>lF$%$2;RFJS`939(E zPa%D8U}GPUN89djK5Zr|iUL}Ol?EZb@G$2t9!c^N4t%%HGoT1E#Jhd)y@FmU4YpoL zWqD)dPfNIF2OQ*%-fxq;sQ{>&37|8M{ZF%0M3utG4K=H_oIm#tF5E78(-fL)KFKIt3-0(A6_}Y7>7<><;BPxr= zWABEFpy3-e1%0eQyA|=2RHUrgIV#9?G*`v23bRGs5@H}ku#qE_nJ*qb6Kp-+t;_Qc zH_w#?x9j)IyxRa`*AWc;M)UDPFEBW<3Y6o>f9xwF;Rq<*U`P2aUId!w>*R{62+URyN{H1p3rMh|R1`wd7>K75q3>mS5C*3chnAp^87e#Bw-u&2QQgH+l26d%L_>Ddo?^Wto+EV~Q&JDC6@bOxr3y5hgdF0=156UE0=l$V=(^Ce!R#%X@@uUY2eRTbzr5KFU9 zD5HnMzEuTr4$qJ8&A(nEpi-RE{n&&}pg(RlXy0+%hq1pNUPoyP_El#}y1(L%s^Gzr?o)212NlW3TJyhUt9Rs1 zA}ze&6>zs%Gki*+%qR7YjR=;Qnjd`#5k7IgELZ*M?q=HS1dbIGu1V#dTwWUW-}&;q zF-Qf?RNZI3&ZaxHEYWa12`*>@3= zJ@H}S65Q9VcFapec{qQz9~HDOXc&cP<~$@kqnX1)M6~k-7zbT`@QX_>3fyo=fesj) zvU-*TTi;2x`}33KF39U`{~T_kVHo+MQ?~^{$XQ@~!UR>e*L|i=*mEhznKJiBk5<+6 zv1M-J&(dBx7hU*fxPkwg(w|3V)RKlaNujQb-ogG?0u5AyhTfIL#ob1=f}!_4Su1_O+RkDd z)g8HEP%g3+{@TK(QNGLYg1=1+$F}9nB}Ch^^xIBdv^@=G3tiOTtka(m2qnwu*4Ntp zhPSo~Dg9^H;~Y&`@JPyMt^jGn}!62A|uV zS}m!02)OLz48us=BL+2v==zcRoftyV<)^@oPmf~EgHWivHA^-}|1u!Dqhj~wRK#Q8 zc|z7{7g$3^FGU{7^x-;)VJB|rG$@h%mj*&#BI}rNo}!>Ef7%F+sxr#g!-j&rA*NTl zBhe|6Dj~paB`Tn|NTxV%Wu6N(ixM~Rzzpzx1@}FWMSnd|dR%%#o=1&SNYIfnwEy9? zje}i6`P=5!9@ws~f7w`J2=P7t#DG0tp4&~$6+&l-Ag*S)yDj?Cr}>zTEspPdLl-C`V*aYlF^2K3mMO2} zA5JQRA6NJ!XzvY)YOd6Nq3e(Nbg>HNlNYCk3FV@D6<8h${ae?5VoxGOFA>s+*J|j0 z8;oK#(L+Fh)`F9ME2&#q7Dx6+e%X5B;swtXmJ2993%l-Nvmn2`9MCR3@DxRsq0cwh zH=R1kPYCsVaw_j7)op>IJQ{%ebyj(+89%@#TWK#o6&{B4c8302YmoiKFT;N3^EXwI z+gX_RRu%|YK-)qEp?tEJh*-rroYj|Fr2pz1tgo{nmPVGXNNv}N`P$0I9;GX8(M><2 z-VGVmQUD>$)N_A)2oLI7v%5fR{FifJ8E4fl~wRKoHPSJi*R%f)5`;Ef&#AMfs z3^Om`9Yn6iB`k>0z~R0(+9iT3g7WT+ zGPCylDy_wZU7lv|pvjxYyZIa|_Lm3|{)V-{ykqx2Z;R*?6!iJQQ1+=e5z86a^<2$* zxHC8#@4VID2CSO@-NAXU-nfm)n{+F^kszu3_qG@Ru4+(0_7_D~oZYGVwNT}Y*sBGR zc)P{VAf8^lT<|C=)u160LK8j{vb;BaB5TLLyOjm~mEW6q9;z7*@>&}+n9}GlV79XAVdwpbuMw|rwtFr(2@!%D}?^y0$4!fAo+up+pwT zqEzUv1};(8+1lBm4s)(+SOP|ZkYD>Sj4lzZbu-+Cq01#nK0_wk>B*so0cxRfR37udV*dTUR1XX zLsneKblXkeLT-#&MO}uzCcq4(EYS(W^X{Z}7o)_o*iw8cule3Rh3BB=@ZOuNigY7LI zi&v>28L!dQ(Lq~*@t=^OFr9=~E2l9J7Ygch4%eT;??YtAAXKKqs zl=_*oCW|s34o?b}u;=7}66n1=CRmY!5&3kBL+x>mLm+Rs8ikWVqDBy|>M-_oZ`eqr zw$rrWa%iGLUW**=+7CD478nea9gk&F>?GbHLxwEf>dr)xG4*@UO#)Lbggp%z%8NpN zimDg&f|3p9Sf6G9qKFuWL9N|Pwf?s^&VhiVI?m@I7k8s$BLw1ZKi&DWPOg2R%?ma2x~dX+>6nx zPA>qDNB5#)--_WefBhegO%Aara+co%Aq&$-o`j=5=a)vyfz__qnaB$jzC_<@$*ALs=;MDQX@_cW zZIZ|$NH``9UESujiBorfLt|q?;oC9&PSp92IC2atnvQ+ozf>IRgHb5*zpjxVJp69H z4#X;QTCpD2{kHAWM}kDUl`Pa~!k3jL8HquN!4Qo#r0)>$j^SQBjKD&mdOn6|=Z6IX z#K>x(Q9;ppu+Ugw>1WYZnRz4ytYS-*)C_5~peKcQ{Ip-?!|Qu{u{k|bXXLH} z+r=~W!wGg)Prxygm|!dVL$QP9l2HfC)nh!Y)9d$4dO&k_+);tRbzF3 zS=KBW%NK!LeYZLIePlf8>FNSiD@BCfW`_4D|Kg|Ovn;>uSypXJsKIr+$c)n$Mc(ml zzDuH>8}Pq;AY}V+-6mzI?!w(-$<%R%=fC&fBxyTBIUCB*U|Q1@fB#K@QN-Npe^ZO< zN1W?l(TE_|YmXGZ&Wia<$t+Z}y(1DK5gKd6%QkwiKP%WoTWNeLRAYAS zW^x|&Z`@&v_69(VEVi!wktw!|sq1*lJ6v^Y_YT zIhmf(gdCkP6vg`*y6paA9>D877A0h{4}#4HxzDpca~|z;4u_5B zm5XG|C5)WRB|t)BK?k$?JO{8@hl$0>gmMy&rqHE_(HRvv-JIuDQkYI%GRUC4wRaB$Akh0&I}*09#u0V*FCM%`aS2rk~TeFD4RUo7zmF9bmC#jb3wY8pARg_7F@GhZ3L(_^fxB2 zWCd>y_=_H~-$gOk6G%IcR4y+4En_o7A0Y|!)+heyEE;6!`yBt?2pc#@LY1Ru0YV$z zv)#Mp0aYY+W5>~VK38^1W3o%F#8+A$Kh8QVGHcCp@4jq1&1!f^W`M#h(B^->hC*!? zg)Ae`5)AZj>wgqE8^l((Oi&Net5l31t)ArywBAr$ZZr3Aj(-%s+n!vQrn^QjWBY!u zf03Khd7pE~4VcQK86T-MnRB-PbvlbtH?i?&Br*7iWI2wL=*N>);I{CEzvo1wcux#a zHlB8bE*RE&*nnNcKF3@6#}v8!s{V?A>fe-yx7~G#wEf{hK8PcuJ0*-B2H3s>s~LpT z0ff8XPeD2qHNJCVn{|I_;HF>l(N z=dd1}WhvUa=e@`m%l)1}R?+6Vpwe#nr%$q8eDK#SKMw8(1-UwiQiYJFKW!h$kq1wU zjd)$bKv6GOoI*=;7ny03AEE=Py97}Ex?^P<2(gz_la)7*_;oi?_T9{KQ%*K@6$E_J zLocm(Yx~D`0JGxUureV1ZpwR4i2rN9Rx*RUo{P>|;|`i`7N}-4VPm9uCHwmks@2nJ(FAkyN%a+3i2HLoBJg$ zJ7#o?|Hg2u0{mJTq@^+pL)l|@qY(QkZ8^q?^{#c)UuIOT=vL!= z(H7(2Kcx7~a($Ea74{wyWPwFKg^|g7DUZ%bu;$xcG|@ES$c9F%@Hi!Pq4<~Rms~;o zSifZhtMr+JKIvinQTy`IrzF03{KIit(xoHzBt^Te51A=xYMYH4l5T0K7m?L$zKy9u zm{#eN&qiV}|4o!hu4EuGJ%EXpT6rBj^hSKjYGl5BStyvOjzgm8MBTYxk*kwd;y}|$ zUeUz*+rV&9%}fq$muyZ?6XwojC$_WiRK=gwXLdQ~c3zbq$n0pRH5uhsxpbCffMs>W z373tg`L50B=#AfU6BIWt>R44B8F3^mNo%-M&}!1DhQXUbKhbUKQCf!3+htdoSKsz$ zOc={67s^l>caA=5U2pxcR+_TH;Z#+UaCCnxZLy-}eH_}IIP=sQ_n$$RsWe*nst zy6+jKO~#-!^%t$7SVE@3A4W>x7*#^N^26gO=-Ud@!>w9K1;Mwzw^nhea_Npe(EboS zIk{NBKD*nUJdlkqtBLIDz)2ctz-!f|WbI|eF8bh^<@f#Xc}Wm%{AH4(&kh_G|Jc*?+1W^HvkxWhs5xK9 zo3CiZ&hu-0uH^onatHGMn!w%;&x8|xxkoO)G>>br3Bn&#No@JhN) z?;1&}Nwt+VGV>7weBE~)=31SJ2{D^Y2Y`JW47+`CDcPgM9B740mid;Sn*usZaZek^ z-qcITU^04df1Y?-f64h_8Dn$lB*k++NQc%BH^<-mEmsKbaid0Zk-t-ka@(4jnm^o% zSV46AQ+iNCDZD;iDj- zr%LgQ;Q8}Wu_DLT?ee=ngQV>FK=F-Oavq$+xVv-)A4d^!Kxui)Xl}GP=Qlh~R{=j? z?s#8*K-+G2U1NHE!{VVU53oOok!4IBknug%wFF7L-Tqx+XW+ZAgsn;%POghbIS zg3eVNbo57YO;z%z+C|-7!2K%+LRPF^W}>*7W5iKX{d(`>jS?1aS5@M=A1R9C~Hp&B>=vY-{E7pq1ARwMfRHcv=7H_-4sx^X@W_WBRuC|`D^ zoAk^Wnz|{T@XNw5dPWVtmup;!>A1xl$ri<*z2|9?K$$tDNd0%k&yE3H@espKV5-Cq znK!)`D&Y;5miHm)}nodWsKzhl+&UDwGSviPN9hj`+*vt0g>420uTn=GU^UY8(vvR^9 zJ|TfzyiO>L0RPXtpQBlJv^$*bKk7Y8T)!VFaF#@W8AUt$62Z{*E{XYt8V;AKGx}DC zNGHqa#x0?6SmPpa6( zfgh9ew9Dr>y}$cxq!9w09fK#pVwkyht~=d1WRnDEU_>=K%*L>;$SypaZKm>z0eu5a zUt~+I7FP+ay)gjloQ&GeXF%xNy&fN z@q)~!PFjIhY#F0Ik1L#gL0dYMyoI*;M1NaI|E&0YXCZ+p+6GJ4dGt&Umsx~0C{1WL zKtA$egWmnAveS+AHS~x3?ff*VG3*Z9Z}*LrF3EPVm}_;D1d$QyDUA9cRNb&4V;ax6 zJ0soDVfmM=+Vj%b(#pQm(=8)O*5Y2(+HfGFvS!*y<}iNBoeax>r#PTUSm*Jc1^Jon z&f#Y^L(UC;gRtTYmY-oc3GdB#b^rc)<6GPT)&6cK2Pzjsm_B@2u5lC~a6&&lyrR*s zc@k_~pYA{f2;V;(ToLaa)P@(q|Ei&W zHz2{1QNs15_1dz0a3v+KW!D=#xyN10_hZg}eZ-Lgx;>u;o^`*2 zCcV8C5`5~Axz$N%-t%m{m?Cx^A=#Y4SgO}G-t?G=a2l^ ziXqOLTDsq1UA42W{(M=taB3WIU6A{DXtw5CKPi86o1OC3h^^pP@*8st)Fi7JsWLCs zvhg=I%z`^e^{y1w6cylD8Cjq#g#p(vq^gnxTpy)c2bT;G#!9a2oGfLOCUhj1oqut? zWgjug-woy1a5j#4J~L97^AUnZS@kops^ieebp@2hy=dH@dZ%@Dlv3M}G{?U%s@BXZ zN&Bv0^$+dUwK6FFL*>KKk9igB&9*oag}xb`>tc1M*~^j#za}1@*<{zQvVy2{qNaCO zU)gDv6=bmmOtfGYseq}u-yQ%l14Ih58d|iHEnCj-VSU4%!c8l74<+-6-hEqfxM-$~wS8w`MfaQAqP)he~Si*{&YzRP6YeXuJ&7L^_YlBWx4|36` zBZ~bE5PXZ5=m3jfd2+xj$N9c+eJ! z8K^rD&mkven=@GWsMoWiZSrR?3vEn772y$zOI#dX$NSU(B=*1mw7Z^@&#QhVkYz_G=k1TJ!Wx4_^OubZc&Y$Fsmq_tx>* ze9PTBQJFR{GJ<$Gb`7@)qTqBjcDk)NQMs*fivysC+98tC@>kn=^<9S%PP-+V^r%z1 zYKxbjmlFworF(x95jrh_oFCY9Be+x$A_$y$%!{YLaao} zrJ#K~%UmoN>&L_vXgET2?o83;F|WSSFR*Wy>(ZcdEh*+Vvn|6liV5IMvjBvC{GFiu z5XE}uC|TIsfkz%F@>+Vqc4-$OOVKyUU=oK<*@Wu58~Wil!>U+BcP`0#ylmn0Z;Lp; zC>)*IGm~?o(R`$}ZPLdb+388D4MNCjI_PWqm0SAsQRY2PTUfHse{al08|z>+89$yl ztyZ;sQTzDQulbOVhe@Owwwz}1;f#F(8#8W_Yw&$p&9^491k+xMxawSg=#+b`Pp{*$ zQWV!Z`aFYpXq~5LlH)XXvX9Hhz|KK>!uq0lJo6hj`fc032&EC{BbSRRp0Z2HDsVLW z8)pTa0y0W-9wF!Fb56W1KA-1-dV|lpdi&+f#M-q&w55cNCuR4>VfT-#5P*)G?iG9Y zmpe{vsbn$j2pyq>uWhAG{}|lF3bml%bC{G)oh5s>Clt1+h;RxG1Q0doj|m*2-MJe# zU|z(5m0)9`z##hQu$%y7@M1nikdb&)kp_GvTR^8uSM)V9rP(D4e+>uopD=6gim`__ z$U;9cnk6X{l{;fq-+9*Iu9Gz*Q*5<*Y}2WqEeS|ytY#m?`g2#fnBzmC0=R%a20NS z@5VSFv+gs^SrFl!l7U~uV}XR-mTcYr$}{SbQ;qtITkI-1_>5}fvYWQP9s9e>`?$3g zjBHj4$yk_$aGFI&NRTW+ibWw5I+3Kf=?bI;v(bSB?{5$(m^ZV$ z=?yE5{C!B7NGf|iW6#MKZQ)*glZ`*KKa!XiO_&J&1!-pI$_D$ttW`P!$X6hx!h0Jb z4oQWp^$c%gZ|90jYYq{%&25Y#)f-y^{nvjY1bILUNOA3*PRIOPp zK(pG28S6)qv%r2=_Pnslkzz)73y9gVy!|eo!Ec_`jrXm(j9Bps-vXny9?8+G3 zQAArk4H<04eu}uJx;$6L(!BC9oN2id&IH&I$D3*oh13I|>8ye{T73|D?6GdYJ72C#Hkp+1juM_UpM9f1TKM`ck(N_CDp2x; zZ@tvf>zO->R{A3QP<>_KXShTVx-cVamU;>mW3Y(TQRW=ZIjV?zSqm>OUjZfK& z>zKr~&W;N0bVc%q&K0!esEcF%EXYGm{xfP@Yo=>B(sD9y=wZ~Fza1l!x5&|&l}oQ{ zRPsLmD<`lNWH*>e#pW zY$^>*A~bR1Bkv$If%A5%YS)>%zP3;?qdof3xZ3)l?XZZ9cw!Bc95jv|!6)LL+#YxA zD7;_Tj)8)DbK9Sc=B~m;#v(kf#wAuymIj|k11FJllue4+aW|5%74xM@Z7>`6BcWy; zu~uEe=>pT!0jO}zQ}6F$3JO`B2i}3F+kUhPgnXZ@YL|7G;qlR|5!lLR96fJjD5_E@ z)uWHw4O4F4aq!s>T{ipsO^U+NEa*<@zr|YB*MhOnbi3iiFYwsiZw2XPlsp*PA7Jks zAsOV=+T2XvK_RK&)T_lpDr^0u4=(Yv_l~0wqcJdcCo+VI8Rcc027rt0ReB59|DPC{?E__XtI-#W%9 z*Sdh)4VM@teP;8?1zzK}?mNYQgq^8A-71Y2KIIO}KQ~!Y2;yKAf?A=VqcgLxc+w-r zNUs_YFj~JXm(SF=Nc6U4%#iv%TfqEcT=dZ$XeZu{MH~Y-;;i?S4|eL+(nwwp(fKG_@6Op7pcha~&%mI=4`wdc$cQUfvBnI0= zlRvu|ub(bqO3H`g7(J$N^!~&vTp%2PD7`PjM_IjDta)b>5j6YTF@B#1ae=!uO^or=syC9cSk!+CLji+u$Ev1%Fgb9UCkfa672o#Dc0;>N$1a8qo5O62{tflf)r5j|Eh1 zl2VvZYXxbu^PDx$WP2GX2R^IHNW*s%kg*Lf*9DLcf)89R+td0iemn0;kr45pn$E!$ zwQ(3}O(m{L19OELmZlAq3{wYD{1Pn#11As6x7@9nuIQ2OntrDs|G?C0e0=p9*?3~0 z)8frX`18ZY4j(C^%J{-B+n_P`)Aqee5dj&%-EWJBDcvO-EHx6+xx2oe;38=M%u(Z` zNvwYwBxf;9xUE#1)kPc{pTM{ss5sTUa0@@ch_|^@m`6Jq~q@U9P|gBs?`1% zg_O*n5r=4);hB$I(*5_xzTrfJ=R0 zOMu#S?ogF*eXX;S&7pZ$Vh|o2U_o2_66l!i7(;k9(|xkTKRrCHqsGy)6YC+sb+tIs z^7W>&_(`BtT5qKahn9xH4xnuQz^_vd!x~ZmUd=nreKw=+)Z<>PD%BL0iW8{x=&yd~ zv3ePo23rKlUL^72Rx;wImAlt(q%5Qq>{(~@g3Rq{9ARBE-`!Zzg94Pu2I zD-^L&4;XQd$q3UlDGFq~BLW6uZr5(Q&OSD~vH&h;Pnq7vUu&}mY&!R2bO8p(TWvRz zWF#uk)F@b~VC| zPMUJ&klfYDOs4%<=Lon!h71NrBX8_sRM7@T^?mB2T%~9$NWlEkShh!vvp4 z%U#%}B>Kai0>JBU0ZM}25eeZ=$~l!DQgr?3nXJrnsbjpqFYC3n@o4a4$+=ulQh@v) zGZ`DcU$vRbLYrn2 z4-WK1KNn)PsU|Q?+>K!xrBi1NSnq{IMP zwC{QP%2Sno(6RSBaoeS{Qt<|+nO}9W3W9yTeQ<*3U-)|(9XmZooGz){o!??on2 zpGzK{v}|2sc0hsyCVgIyzCp}SfG z-Be`JIGFyGw`1U)x#0|IqLs2#OOW^Kb}ET5RsHVo9nxe=(*%#*jM=YViBF@wQu3u4L)q3S_iN!q4_Urxa&a>1LImupVBC@{7fcv2Y*TysRWpO=9i}_gV2K(409|_@W!E1B010Qz}p05pX)sq0~*dv%dtn~c${r(A|_B*@9${GJ|BAypV@^V z;F4djqib~gr3Dig`1}JgUz>$$=T0Q*II*I%X`&jC0nWeSWl8 zMC#car-V;;l!)Kvp?UfNo4_)}CSqBC6M=GwRQ64N;f+U`$~;+Y^u+2v*>k#(?VcUK z)`O^M1iN?Bk4K(dejmC2jf|TRDnijtb`L92v}~*w-$4DET9uQt2lPzE1c9whbFNX%F_30J0R zRT^6=MwWotq~M-(jNuQuDMj?V>#^+oJOWUM+F;>j`&Z@;%#oG*W$IBkywIQ>Lfd-| zON$@5A`edJg#Jf~h>N)?rz0RlUgxIVuqQV{dy)n!ZYX#RHplrj5Po`Yj;&d-su=5G zsI(VH0-MNb^A4+fzT5S0q}k;Z*nb%K$dZi=Hs63mp^EuvgRZO__dlz`l&QiPqY}M8 z?o%{CP0g`8S6igfaFpg0e)akFyL8gMq!IX{+omrAh3OVw2r$G?`PBJ;NPX0$iifkb z=dyyEvcco{kZ~O-kY|Vo*Tnx=?UJgA?JYt-agS=Eq=^)1EeuoXuIMwFGPTpi1d)54 zYR>#KIw#tO2>z`6go`>Dg+?aHUHUQBXdoa^45`H#nRjZQMrWCz7436&sI z5+cZ?$=&&F44C%2&#_GsBL@e6iY1}XVw8b#jzG6wLGUw~bAEzBTiA$KzslXJd|Os4 zexNg|vC-Ddvb&POw4QyLRb0!)gQS{8kHhzq`^|Nl!8#7H&?!$F2+aVlk#WSE&X;}f zp1uo^x!%2r^A9iAm)SO~gi*bv)P&i0LF!Rx{%M-CHVt`3uLbL-=Qh&cZpuiRXWB}U zlnZJ($)&rm7wo7VBxX?OEVZ$;7GgCSSEaGv9a<>|NzLNqZOo?}AseB>vdlDcstt9s z%z9lTL?PsfYBm7vvfRyHyCT})yR++_afd*wVvV1$cRuMs>*8mh(YFUO|3k>6R2at0 zm?lpP#4te`T|4$ylRqesj5&34ST-OGi3WiNfhO1Mt2_?0=$9p?RU%b(Zt{X@|DS>Wa3H3zrvv zM>BGzy$w>SWu48E*$V>sM|62D*1+6P5J|;uv*zP4(IW^QH$e_{#GC5%*2S9ZF@R{y~ytE0-hU=FruXS)L^P2 z!psV%P*XP_CKDYc8GW)#v@yUA)~e#5=>mX2D?FuU{UkMe zK*sOS6Z(^vdiHLBJUV^jp1lURzI*h(8$xz*qw!RD-z#(iq?^OcpP;j6uELy!Hm_G~ zKIy&$|?D3XZZA3NNyjJgNEJaFK0 zm2c7dOYSa*cW8{R%jbFRb1!o`>ORddHe^!NE84ehZ9&Bw#7~YoFk)8;-L2oGuwV(_Xy_S6p6+gEl8#2~Nt)dsl|vDG+J&Ca{S;|i2@kWG4(6(!oG%>q9- zF%a7C<`k;zVN|BlgssMWe!_;nBhT`aFq9fmsVj`pFNW)Dr2`cNel9f`OMtLMRQc^wnzWq2m>GZyj z3S1-`K7he$GAFc6hNK%6pK-R8T98Mp9{gVWf|N*K%P$-i7HAkvsjn17e)J>HN;akJ zKJeWDAEOk`S0sa<^mSS+7 z^`nras#@$K)1XDZuCcVq^1`jEoY7v|Yx{1kfNm=URO)Qa70tY!;OvH1=c#Mfqh&xA z``$z^0Tvg%pWA|))?&k6YS$Hzb}cNY$e*C{h9gXYNIZ406M6)%*N#e>Vb=}gJjh?& z_gsDe9wPnK&m9TWX^Sd$e2vd2WS3>DBDDALcj4-qfDR8Y46`q z!8`_qQXsBCnGR+}6{A)^4ypa@+zt;i_Iz6V7=^cvj5hPc#!fuiF21%blr#w@Gy+|k zhDQ#_4*1(&=yK!gq1T4;%x~c#v}w6O+FKUyoo`>`%+;UuxsuQyvVt%|Mx z8z%}iXqsvYQ!DJ?3QY)DD+RUHJ*q_C3njj)bFHd0CJQVoEV{Q0#jJrWK?{C5kN`K4!cjiBQhj3O1xF{V(i_=+I>`WP5Nh ziiZpgS9j`1g_h`C?_rTr?t>9Fu|=jUTH|^$)cWl1I58)(F|W46i^5rF^Tap8*f&Di zD=%=(}sbBAwan6SJ)z|FY#ou()p`K!)a{oveS4L=ITP*{00n-cOhBZlqreaF6?1&=FBbCSy9!0f7a zUWI+y5YS0dae?GnQPOA{fDsNRtu41W45%~Br$C)xJ{}$^^PbN&F?kY7RU0lzCc&`% z7!3YzZtmb;D7Ra$easO7p#=+pMX8ygZ(d9UTaGv+fiJFEP?N69+suNDy2Io&QNAIW zIHJ0={n8Z58sF#9r8a4~*tE^Gm57ibLE2HiWzD>pfWM_`D_OGtzUF;wG?aanRhRn; zW&~rao4>;TXL7C6!}ddqqs9G%e7!OO*?ulm(_RAct9-l2Jk0bnH@(v0CrXcl?kmiq z0{OUk`Q2ABe@k1B1rml}y1muc-qbE#p@Q3{<`U)-8|NhhtRQOYa#{|PW7_myDQO?Cns#Kssc%soSRgI8y#B zP>Ydh^*9vS>a(u#55xyQUQxg7oWbB~M76&Xf*wyeFZL-AWVP{%Cc0f+xLJ}q(Hcq( z#3^OM>o>T_BI7JD*M~k!{#Fi5p&>T2Vyhr}E9iDuG?L&p+|;IBZ{vMtlA-Z@mGInK ziov13w(|XVD-xwIY(0%*9T&r1g3e0soaJ^|Fqf&Z^bm9;;+cdU5XU8a%))vt@wd6u zSq7Iq8wTmd6YL=#5iVDtRvllDr&X^uC?<>3!;{i8b$BZNVu*OIL!TtS={K~i_!p#3|gZA z>|c#lRMf2()PrW4F#ol0=|rYDQwL86UJn?OeV-iS{;o~md7XjkTW8ft;%E5y~`)O`fiGvVg*T&Cm2J!7kP-xm~C*fn9%r#z>3H6J*}-tDclEL6mvKj}KXP&GjZEtP>-TXh z(u`_IZu#b|C5UYI`c1~i88fxLo}5(f{ym35muon=Xh5LANq8#B%$zmrdTT59ebwFE z!{9)*kE*BesTNgp!*U>hbRG+1zPoplcu!vGLL#m38GMca&Oj+3MarsU7YYvFXZzh? zV*0TKeD>l(@E+ap(eE7w+m$E=ek0p2>Bl!9+JKs$yxf!x-C%_OZI`YAb7t06cX z+Cfp2?ci(giGESKRrL7%vs zA@LX-=DfEpqUqaT`Qb`~ElbIYSPVhOAM0c;JXSXO z=nQB^1fNsVa|}*S9H)MeKNDS|C-I(ll?&#zKFpREW%=63*mXXf@_6O&yIpY->#dZ% z9|Mkt*r&2F$e)uIJhW&Ast!UM>63;?CW;&3fOR+>B`b}BxHH6Fa~sf(YKD@Kc`;ia z&w$7d+Yc%Vx?=eO)hCSjlJe6Y#fK;Q<7ttAe2%TOSFs zMV-eN?Nu+uxpM1mK)Yp*a_kF-`m39by~(}Adr7wm>Ljd_qHN5l_0}mWn)JO{S-*J` zcvsuzWMZeC(yDlzkEo89;Lq{--B)%n!E4D;6;_{v6wv9~$C@a67f9L2@rc^8>zG`d zf|n!BpXR3~gZ!m4h(X?9Oi!?=!E`(?i!69SJ$cSXW8^+DgHaeN2~hr4Z6wHrxu78B zXTwE)<40&dbZz@dzp$LS^>-)IIve=Nf_A%Iw`YoAsDKK7GStzucGM?c+4uHYFpJHU z*nPguc$nGC_9L31EA_lI@#)y;2ze2kwndCp+oy1@oyc50AvnGXHZrJEd&5x#XIjNk zy%yl~WOM1j$WVk2aJgj!L|x(a0%c8mW-UPJg+KfKh;&x{JI}s36wX9Sn^d!&y$)m7 zrI;6g9&dX(c)rG{YoSRKYxJ5#=x7N`^qISkwB@k~PREen=R48Y-C_!-g8HH>k4Vy} zIeNz~z9?E|*x@FYG?ju3qdm7XdrugS&?eY##U$U<^x&3 zv%MsVQUFPQ;5zW#=H>Yi3C2;}<@t@C*X4s;qel!QOs%`C`JmstZ!(S)VJvs)sqd!3 z#UT}MjWg+UCoS^#tIg0My29lC@?{~jC@4<`SC1%fh-6;d!luJh-UXbk|1%Vx3d~pN zho2U;;F+M1K8RsVjID;I|Hcz*?5v8RvTs&W+hv(OF5>RwMfSo9g!!c@;x=J2i@ed?7Bs! z<+fTb^ljUtb8XD=`&KL?!ZwsFa-y!;RpT!VYMIVzmf3di8c@HmTfvcMm) z#YZ_4zHUdKbToWmx=bZrE)Pu99TnC9opP_~s+k6BU55{_rc6)WJ>ty-z^gZ2CE62h z<{7s_(1%2q05TB- zKEqAT@O|~Mh&Ouj%Kg(_6PRdPaEOdGdO}|Y(r4Oc?Us=Dx29v-Omo&I5=-&GNLjaY{){G)AN}y(eDX_ZVLtEO#&C)f7JUx5#pHu>Nw{S_PCw1Y6Y9_PUgoYvg$%!lda!op?YF-l%f7u9#d zky7=OkLaT+y|GsxNgK~@qaN?j_OQKpTzYQl5s?E^Gv{2>-*rwwr&v;?O8aD3x!r~W>w{SfM-RSGj%ktWepCyt<85Z5=~h2$zCyuqq+vTR-)fa@7lTcrI@D=*ZmVu z`^Tpo>8buhYU+l6R*B6i;|p)t%>moD?6}ss%yph^&YhJtdaiY&!un1|VX$E?AiP=pgcK3^y--_)GO9ya$HU=2N zG}eLg5agGE#nuDFCrJQ*d+q(2Z0`?UkhS52C;Ba=Pq;c$UFP7It$C(rD3=MyVuHp}h8>U)EoF9zfs;AMUu z%mUaGQ>F@I#j*5S&9KJRIb~_3oC0otB0DkIk%ro!UH#iY^4~Zal=KdO|H`-0zdL_d zwXitfHJ{YE_vf%y9G5nut&{j^{cMun>V5Du`n0@(SJLm4-A!x-b#=<5kzRl3*Ctmq z(D|GPIa=0M(LIwJHmvo|pL9Yo4@R3AxeUpCTn5w6`Y5DRC@#W?{~K93;(G zS^Rx0>ksWtsn!A->sI&G9vqj7&r9f22gX_4X72k++x>#~xBE?B7Zk(?X%@@=F&UMP zcQiD@BlxwpUO5kY`C9rm=*4dB@B@~o^^=qFATXWi?MB{6L(|OMTU)PGfma7^ zDJg4VU^f8>tw6U|IaWbP7H8I|oC}=3UuJk)an5G@wdzMf{RM?hH?A|CjVHD4->a<| z<75M>Qs99!p}DoF_$91I;mytOSG7e{75|!3>h${E1bG;H2w7kh)URNg#rp~GfdL|& zsE(}7IcuB@7?fo@D%)X2Ho7hH`bjGK{VMtbKd$ZL5q-&MZtOny=ueE!OJ72cz22qn zsO06u7Ga79TnBMCL$l12LZJ5Cqw(jSp>~F3CjuPpt++N$ZmI;tWbxyi#APx`&a#Pr zeP1ek6uTP&LBpHM{a^txyNQ1#%M zj5=xii<__VnqAO?_NBdBeGNj`3(=8{8IbwZrxhbPa+yD7p$X%dZ@s$ zd-*-0cROltuprB#3`SiVbCai<`gZQk9QqQR`mV+|&s{a*aLc`Z{fxr;$86iicdXbI zW6U~$^C3$s(eAh@aw#4aS(mAk-fVNYLq}n`BL7t{<&Bfh!;VLarbNQHGNDkAF~9P0 zZFF8fW5t(ZG40Ny`o!x_XfO3$8kv%|ujckfzjH1zi{if>lV4&~$ZV6D`)WAnPc8f) z8K1Ep9g|m6r?(q(qlla#nH!Gp+;#z1W1P~3W=EAW`HW)Qt*`cVwj!F%2AesUt8qhb z*7{qjyh?AldbIp@v$X(mk1<21%&bZZET0*MPx#GWIMCUi%28pgM z#CpGEi&2(bUyi}r4>O-u`?KuT{qPN%{n7>t58yB1(3TRZmb7{_rf%b)4E#7(+dLeuC;$mf#MYAL1%#W z*G>VN-k*@MO%-|WU`2*ah)LFq7h7+K#XhO5c@OWHC}yy~=wjmk)vW`gOS9j^?jZW6J6;iw9A*mLT1Y5tOvKN0sl-6qhtsb7n57 zc;0LM`YI|X^or*9+A?fM(p6QH71Y%D`fu*Jg|*edry}5uKceT-TOBNp$T$9r#SSOJfCE~Ro-KHWz zmwKfWpQznZH|A${>STnZL@8;LrAXMSr#Z^|c8yxh3pADL?lo~&yY|&Ml|C%9>IY96y11`*Ugt=n(i>nl8KdrtKU zA=w%w{#WQXx;LaQx^Z#=+X+FNx@h7=hNIFuti-sTwf9_Z z#F_i6I0$5~lr!LSx4)QwJF<8K8^>q5S}Gl@$qi}uA9~WRv)-yJGCQ>!qi?_KSoM8os%~E2y^`zCA!wO*tQcxpJzUVw znv>R*kvF&dd5~C_W*ZGt*gNqe%RnPaW;n`>@Yc*Y&)7naz`(EmZI3gYTpzb>N4EW5 z#lTChxoW3{xD1WnX)F5}RPR=@aqG%|aLr*rtdpTSxr@th;*xlO$|(~GaQ-olX#}kT zR@;Nj#0w)({LoEkK)L}EpPp2pd&kU&P0=knn~WMf6tSiVPn5ReENfDKfY^7k%JC&< zWFBDmKm1h5c3VJn^f+vV9jBNRuoqPV49d#-b>{|WUyk3M-Q{I0Db(@rl@ySErV_*V zS=vaY>piW^(~+_~0!h4*v1-$8hK_KhD4s)*#LeK!a$KIE>H63 zSjtju!03`k8B%2q)JD8J-jUVlb^83^-Kn^52}4puj8)c0ON=0p{>5%4t7JsX7>N>ucpI<8<$-lHXnhAsdGt92bz{@gXWh zf|lH~!F`8GJdN~IhI;sCa?`0#QHgP-HNau>FJjPj>>H60hnl-LZd=lag)dZlQVRdKLBOB%bSM?^Q*gZfSo$B3=Eqiw%=p@hIlK=RDw+{16wJ6ai46qirX%6|7Abl z-}tAK!Uk$clZ-u8sf-E_n)LHUD`ixf1#&+PWgdHPvPp7w>)*NQz5 z^4a8v!Fahk2v*X(`yjl&|GioDmF5utQyvq-hE_2(ozkM7n*B2l+kv&3z4G71skNv3 zCR?LWW7qzp=+OuxMP;|%oNwMg@*bZQd%R$n*lvQDd{Y$Y2cOmLy{bH4sjI^HNtQO{ z&1c34=u?zlK3nxa6J7<^TmmpHm%AbItyt&9F9dW%yvwYE8Yu;SNT0W$l{iyZVQUB~ znHR$zukd?LM#VofpJ3&p@r7-^$SFE>wP<#=-CD#scgulg)xTP5?R2{W?oN=x0+R5=zy?bLr&5^V)`RjrbmUhIv6cc4m^JcZxQ|WQz z;CwKuU#YIb*t|8!D-#A&?T4phz4o>}Wj6LUtQb^EGij~KSFTR?{hSx7jrtNHhB_lF z&q{kzc~C-&Vo#2`_Kh57O`5Y)rq-Jb*LxHOFmL%Rv4s77k#WNk1;g^H3b;&jb%(K* z2?%NIPxc(E+*fsDGKvKKDqduca|B5b%R`LAm7S%AUJ!DA7$SzFF07Q@Kkhs}JQRHvvuR~bTv_jmFf{f)|J988LF|s%Tpbmcif)}Y5(x)bif55vzK#8Ag^MD2;&5K z5|(C*FM~`?NV@QNnYP6Q3v(&o-}q_Qc7^IEM^DRNZV66_Zz7KmmbJX+rdTwuAMoSL zC58%x#~Z*&>D@Ve!F#_R*4xMwa~Y31CyYFjpt2d(dP^b@{9b%OG?5HqCNQu`nY05Z z;|f`KZ85$H1oRC&+iby~p~l^~u&gn$D)$tLYWCl6sq`%2DT#W=hWLjL$2gbmJ`&lx zX$VtoY+-HH^B_RGpkM+SXdyPev%IuBUVrb7RmUK9i`KT<>1^xPI<&%xh)$<#T zkf^feIa=o^;Nl(I{@d>QQXVtvH5Hde9WmbeCbM@e82sC3DxWAcYKGkS2I;oPX4vY_ z>wX3VN=q0|FGEswH*C1?O1{32N)({##L}eUqiPi*@zLS}C0Sg(voeU~P{xONOU&^1 z1fiXSAZ+dTWDQzf)1^b-nQ~SU_ zWX*KXG1!S473`I;_WA}$yM%j&5{8}7k4Oh=E6WF&0+sM-d|KxO`r;>=Sv9}M^-4AV zY!BmwH{iv7DRwQIaqOY5(*(yN5bAsBxY<`+{9}?=PC8PIlK%%A2 zmRq63Xps?W#1Ay3l*d?S>j~|MaQ}%v!$`FnMvDKe^^>c5x|@#wKe}?;_NCyHJ(;_= zCyO`NRplji3ds9=7WLq1pL%x!LgIT5afrrvp_Xspj7M7;EY`Ta3Y_a7-%i!$Ea~w# zfZwZB=;8mK)a}M7x#w6REqEs0JKDOBJ@T1ofX0| zcwXnEwhfw=3Ar}DOK&<#3BZcVTuY$;A46`OZ9lwni7KO(9tl@2dCXU1lgo@ZVyN}g z;z&>D0~?xx_CdCq*$k!FukQ1_UImFJm{ZobjjZuR0+nCa79s;$1G3GzH;e$XW5hK5 zm7YpebM?ml?^DioT1|G{R%5s;`)yF0)lPonWtaZgg;?j2k90>RXKgyIm)~Su9{4$5 zXy-8KBDsE7aIx*BEA7QcLwuCB6U(mJd~*tfKh8ygpT;r-JGRyHJ!OTDJ@~|amG3*F z(ON1f0Zks~OzW>U+4NvB$kzxh;cTtoz>uA;Zq^A{XZ#jCH{|>!C*22tjc2`vs5vI% zL>^y`iI zf4wexK9)E=0*15JwC=>Kw8)(pl>o&a`RdQ@^KGde$_AB?RR5 z_od@5l-%Z}`vHNov{tgbRs#WM#S8zKq#T;KBck1V@}Tx+QJ|*Q@4w@TZR`4zq1}oY z`oai0(;TP-Z?XL?LwC@5urBA0j(MTKfLK@@3nR3YSoTBIL)eJw?+HS>f z=Ef{WYdGCmkBp1m1WW1qe8?U@Sa)6cb�|0U+}K)bI)0-9N%{92C!W$V9(;!~YcR zVobw|PAlioWF_OHa`@0($@4|8zT9)Pfi)&zwrOM2rno6}U>C6&@TFow=>vnU` zjpbEtn4J{2ob%xF37TIl#afgtl<*|#&GYCJYGxO%fxG*wbx-^z`RC8P)2vA0L%Fvj zPd*PxohXg7Kasu;ylKws|72C)`hBZ1ClC41*U;p)x=@)kUjMzml%ZF!jo;F6iW2`Q)UyJ~mZWbzZa(QnxG;fGrxj>p`97e}^Wat&HIvKdZQGdt0C4WgxC@G8rGxsFhwu z^?LQNdK;^lS?==JMK|bn&$Fu8Wm57WsdP-{Hu?$HqZklMcXTto|HBtF3R5yV9 zixRf+$Ulbe4B4pk+}Z!qqW?e!A-7$)`Q_>?vUA2`0+bgpm~1Y<|4d~+m&|Tx{@cmj zSoOb!bY_9i)aSuISD7)u@)ptR!PcPp&LglvT)-{QM$8fzmKrUn@ADg^y#c_3WZM~> zUHUwp6Z*f#tp!2(f$r!*$Wg09phYJgf068)UPY8|D9pjg%$j%vW;szhz2+oYnatIU zqX+_uP@9<#d4^LG0dJoFI;Uo>&c%}Q_5PwX(JqRI{iQRI)kgCrn)n#Y6R{AC(~g+( z@;XpG0u|A4VW{&8sXqokt?^V+T~s>?iTkGH!zZ`@t^sBx%o6P^;l$QMH^L!kSgrMJ zXsbalN_5>{C`=b1p>s=e=XX>v(0<*OP|hiRIX2+|!+xfsM{jS}xogA>8``0USvO26 z3UK9xP;5ML5nJcc*0eyQjebzlv|ZkzJ74D&HdcLBqLpP>x5vX$#M8q!JJ{ORw5!iL zpqX>!we6#3yxb}q3zRu+$bcxu^#&Uh=*0rn01cg&f#{)9%TvE7N!~X6Y5OlVpOt-w zm=g&?W0c(iJ2t^`gn;n_`Fr{65A~Wi+eDZnDVBeXPYZVWGBuM23mH zB#h+$rnaxf`}7#UTUZV?9g=#m3=3In%>YzhVG;gKwL%se?bbZ;!t+xcsK3md@*b6kS z56!-6sx&eP3PHnEt2OmE%VNL=(L()j6ihz;DtKbPWUmc9IUcQJx>oi&P#GuFY;8Yi zq5Jobmq};;!qfG;RUAK1zA<@r0lZ(LhvbX!Fq$G?jNo>swAG}1%3rC;)!5s{3MU_x zq&As|3?sGA`W0SA_MHPZ9K;E|jqxh<4G6vWahf-7Fps=#q{*n~0 zUN%?{+HXd3>yxAqvB2k$4j=oaQ>ec`CZHeX>Vbs*lNAX6DT)`rpS`%8K)WSl?+zG0 zQ3tFV%vc5h)zhkBn}#Dw+>9>@(HFp+%&l^qpkTV`?E0b}{c9`q^1S^T>F{i|Px8b# z_if0nt5jPJWeIGInAD3Z&;XgVla9_*hCgf6n$QnPKrj0mh?_&N%Hb*H!TTnm#e>|s zFjSO#3Ul>N-Co^c8-Md}{f_vnbBC^tc^LD%+w0eXLRT(<%HMxFw(lB`)!(=-CgRwV z<#Em^pr5R(UTw{UJOt^Z^Jv4J1p`-O!FIm-G5PpFle>#tzDK_yOk>$WjjO@wKJGKs zW)@3av-t6Zkm3m1|g)ZezT04mazQbEaYfw#LNGZOT>S0JR%}5PZwU)KB za2YsZehnaf$`iw;q?!x7-|j7+ZT=Qe!;)#SB?T!Gh!(h%Srj#xB7-PZ=wOi&;xVN7 zxy<^meE|1{e+*35{Npc8`7E2_w-rC)nsy}=I&sCtXMA@KMspTi>2GGx=rp&x{W-oeDvbG9(X%GvnQ&c1QMY)i*Mae8uTJq_hUv&%hi)V-fQB#DZJZ`G}&8${a$gsua1$j{` z0Yo2i^8SFw3j+l8`v~aSFp7QwJ7ZZoR4W15s;WdRL7{u8%L;NIU*dJ8kJAyb&65RC z)L9d9LJc;F$=A!H?S#GaJ`f84%*~L|LVIwr_Dh}b$vD>H)UF=M7}&pkE83*LM|j&G zf`e#ldLobKKzCaKA!q{}ai*93qW?L&=yV&;TUEm`94^zJ7aHHF*%24$|5LV5$5W@5 zE$t=OIw_Tq|F69LpZm#e?lsLgE%}@F{i;?CsWXBn$UdZ^-iUl0ze>gslz#f2Id9)8 zOPdQS8H!<7j*CXjm35EN$*VGP;nOx5pVB-6L9La4zKbP+iZEMgC$cE~ggChm^5VU$ z;0g}`qCRQ2vxjN+KF~$XTWX{qzbgBS*6uoN6bzKTnPv*8x-6BsVDZ}M{~Zgm;Bui) zgUSB_O%CYSdreL@+NOTrs%)+TFK+C*Rm*`W`U^0?8&PsXk_ zU7;(>siM*GPaAQSR=UyDr7tpMU39GKcBS2!zu{@iYSPos`!_OykzldsncibbsPxTb zHhb6IhbMz16w2n_p4RV0IZ9iN@WZ0IYAg6;@KyhN z8`3%6p134Rwycg}XIJO&0NYABDu*;k4g(uwN=L(L+=%yWGTkqE4*8z-H%=_GaAEsu zih=m)xF1hy^Sa_SpsL7z0v!ZL;XE*2i;>#XSSlG@^=HeBP zm)pw`%XViL7nEzjVm-S4ZhCUBLg){^{=vr%wPvTnaLUAg3u*1kmMQX4(Yz>NzO!RC z;dyFT`bC7-K$OJ;78{r6BHHW%nTm@(Wv$=U?c-SBCeiq-Z-tGy>Jm+HZONC%4~PDa zyi59R(aizU^B{zK4HQL-2WLMHPXrVmJ}L$CczTwWz8;ZNqM`Z*N$x#n#J$CY;~}ID zVXT-6{@uWvjCQ%>sIa_*23ZP+pmi6xOwQu{-f+2m zcO9^m%98zHO^xX`vVFn9uIfWe`u>&UVm=^}N|<6)i&huSej#Yd{llQzT7KS8t&=XB z+nVuZ6NGyx7Uy$OM*L6nPX~np@MGe#M@o4_MQ$%S4h>G&JRfHKgO9O)X_-n&514p} z08BgerWH;{kG58Q*HzXDWLs0d$$()u1w6QaCwQO=|5_VYlT(Who;ua<$&5CFdXN{N z%v}Tx9tNJ431UGdxD5uKDxR#)|LUvw^WO%v6Is_uI4_9U)_ft1>sy;u#4&5*@8MGgohd`t*B-*QH@L`bAPtg}3Xy!FmFwi88 z(^(1U$cjJP|!9xVP|#H8dM<6`#iyjea`j-%%v zw{BUVYTxKKmSyt(UC8%izp64UIO@;!ooD)!CVjKIp+9Q^MXW5|^yp$2p&bLq6Z!rJ zY(u*6+WU9`A?w}JdC~`hx19XJwU660_Rkk4%VaLXUEj))r$@g#{YtJq+LEcGSNq-D z$hg^s&b=?n6M!QPqkm_O#HUZ?j8@aJ$@8&5JryBxX)IYCT(zYU8G`E$bq!xjQ5liK zyPKF3x_XRS5u+gHrbCc}r9A0&lvDc7@wJ<><^*dV-5)VG#&0(w^x(1vi(>*|Ut!C@ zoE!_KQKeq6sbV4DbD;?eL{YI=f4PEsF)5Ihnt;$Din5S_NqlqdC_moUdoiDXyRnIcD71(opve@*4Ykf|xdf5Pe| zxp_K*YEHm{HWX|3+al=Liq)~z!XFt^aI;=iNlJBW@MA(;$$~*LZ61L%3km4uPe16G zkq-LXB&V=XXE;ooVWFMu_G@T)xLR_-m!Dok7W2MYLG8zn7BT(jcIQP4*V(gP{r4UT zLm3lE9#A~AY&|D+M*P>=1#Ou7L!{3Ck$70@)z{XcfUKCM7Fw%yEmSfRuvQR9HOE_- zKD<=|ujb5DDf~Xs`FPK(?T*^I=iXU(To*a!Wc&I#X@FutR-<<9|?(@QhlBDmE@yE`}!?@y=n^Z-R zKl>!>bNj7N$d3kC@D`PWaYM&|#KM`;$>uDRO|z3nw6l*Unzb@L{MUnBqY;D(V{Imu zOr5nr^)}YKOKd(lriD(oj(VK~;@+!WIR7eX6~i7u&KAbkk5qj^ZmX_uF>=M$^p1!{ z1D9mqJ|v?bl}t~+Y#YjcInAqx{0WB`$QJ|>_tS+>=oP>YT{I%@6?zuJ8$D%CPKu63M5POYZ})EwC+0x+ItH#n4h@dC9w)RzS~nsW)h$16 z2BzaaL5!cLif`xH{;+>#vHP=12n$U$8xc^z4+JK0~^dqkD z(&vV@RV;Jk0{DqLr%9Q!dxaBm^5F^dj8iJu3X;)mqo?!S@HPADa5e;>rMd}4?w;>l zx}$#uvtVSAy-@lH`I36cTb|w2i%r20`JhA5T)o+c2=}ZvgZSRzM}n=U^JhQ!g;~be)F|g-Qya>>J5^E1?oCsvAlA$C45?A+e~I%A ze7@bgrI64fOCmhWXHEHL@c5eyU$f1NpdAXY_uH~b!E7w8TpBYe49c>0`^{~4pRmgY z!3t%ouiF&JA%`t=^@{e|QT*hV1(LiaZoPItG2U-L99rwrR@|I%B(|AR6_Nr#C#)ik zp`~XMAeGgzhf_!Ja&m7cT?n*Uh(K(AyKb&wF|G9=^F3#Y`H&*Y6bU3=$<^(P9-(woaU; z#uO-QT)-|4jAt(Nv|{Df_E0CS80V+)d9H-gT3NrOhDm>Gt=!Q8PSS+Mm;m799`uiJ z*h3y1o|85^^2_C++#<-BUao*oek{VjP#aAvC7i#nh8Y_PeH>ZPW@REWaz|#2`* z{Cf8VT;R$3_nt6Hm2?W9IALJtu4m}Og1A8h@fRT~E*UvTr)?y$lkug~%8}Q9IF>Wy zjBt$e?nD<`*nS+HO(J{`5$ryAuQ$t2jLMddXil2En|y$Tb6_Lq(yCOE?n74k{$>CE znO$cS6;q{}ooTuXYJYX=izl7&gp0DZtIG-M8rh zqO?DKRJQJb#1iu&=(ST01ls^BvW}~r9WQ-Q^W3)oz!&Lb@eHPs&54kWI?yMFCS+?; z2k{cM?tFSuuCjO>bveHnSS{QU_XaxnxuuqP5B0S5*pJ%o*}X85Il6nN7L9KczbUuhOW z)Y@F*?;&HElIayh30dIpAH~(<$@FLyI_)6%!;|*#jcNKdn|-ebRi@>t#LSb&|EqW( z!%I)|rj&a0Rb3!S=h4JW6Mk|=tH0T`w3ISt*zW#H9#5KR9l2?{Kcse{|3XszNA(kD zsJimB1wK`;HefhUs&3yANTgXUY^vJ2^Z3d3(&FozbzLl5`|*JmvG5OmXFNgwb3bfX zLLehS2F;Iv^L^pa@@>4hM8Z=X0v>~cfh$m>_tn7MqcI|&J4Y>!LM5@ceJuAdo{qPV zZ#`ZXL3j?%MzV_2u-c7G%sgJl2(S5u;fV2jis=V-1%SWNv{o*o7c_W}Zy5+8iaspf za4DBeNg6B1m^{JJ5g?bx!uGT=rf|N}8OqtL6TMgSx-aBIZVJ3Ax7v_z`-HQ|1X(*e zQ{rOh_=T&sf0mTEh!OjGXo#EZsBjyXPeCK zN}QX<$x(vD>DCSlhQ+KPPdT_X!+32h;dI=uji}c&SUWqlP*iOO?#NZs%+X|CRNu8NR zU)y_H(;Z>0%*1jiHJ>rtpDGygmo1wghJN7kq*s=Lc{u9LkyX%b@JWG6o7Q9ED88p> zHjPJXyt(Z-QBGyEIabzwbbIl_gP-UHv;czjra}z0@46NJ@-OVc-@^dDh}IP=Yb!aS zW(U%HxV6Ln;Y~solzL-;ter>=WO9K>kXhL0|CXCxg41XTNrSJVJ z*Fa_}*QQN0E@|rXi%As{JT*Mk_bm&nRI9@2HdV|n-W*I1&9+Fa1;i6^uPMI!7|VLL zreg!KGd3Jr1D8U2DXbvVx)~CKMP7AgD{4*I$j=xYDLyC{JuEM zs+9gR6AHM1>K}?Ib6JiS(@lF@>vljV{u2+##t{d$j#-foDv%#8&1pAUum8bWCST5n zoD|X8T|R=7GKc=uSNw`v0FPNcPE|Q&Hxzj%>eb00=3e=W=C#0=nl(uY)2zW0%F?R9 zTl%0@S_{xfmy_^7p*y+N!l~&Zde$JklZNrgdqT<0Rh$>|UVd~RvC|5Tf;yzhe?5??FSLvU3b%AZ)U z`%o@8fdkET8tU@(@{qNB?yX zUS;CYB)4Ynv}hh-e_QC3`TqH5c4NYCfR9|lpfyc$n}5DsneydMQy-SIetP=%{u1l~ z3{NXwHn-~s9p?s_NM#eh4D$Frhfm?8dbF9hurCBtJCoT+rJ&3HI@c1e<0Ndg@^VNe z=8G?$9LM0Sp-O~;jlhLaJh?SN#_%H{OS}Fo`AHap`u@!2fj+k&CMrJ$cf#?Z8f(7( z8a}UQPL4hpKkT2rmZ!k2QL}?<%qy~pUdR_lDbA9)^xoy$dMo}-y47o5kx|%FJkm}o zwt@8CFHp11-r~VY)v;CPS18a*Dk}ZQC^0^RX$SSH73m@DDc z7yF5Jx{#s?MDR+Xkjq-WElvz3>HeDw0Tj;B8Y^`5e)%jVOka-F>Q=efU?lzKcxRJ%=9=$J~KmLPgbALdkjk^qc5}$kEcTAp=3bt)ubl zAXmNDw}XGM(IF=RDAl7#oZHOGN55m-IhMWGs6G`yP*_Kxiik?Jl;Gd8J8H@Q;_o@I z*ibwjeA-OvT$a3-QIv*zT%b{NkKA*n%TW#>lHi(Sl)NFnd@)L+K*Tbi*KZXktBnI; z)A!&&ph^k-yGlh>&!WQwPS#K2RtZ`VyM*0Bsk#OdN(47zigr5G#gQNJ`n_TGG;o4L zxNeQX+PLFoPTVn^Hf=d0x*OH=;B25LRmi>`QV>Y=(kKonF6`DLn||-p@hHCKPdG~oYfvfIdz1GN)Ujf{u=lQQQ{Cq3nD+O2!pg8drF|DM2~+)V!#w#piw zYq0wpKcq=b!(Uo_l?2~cPU4p`61(5#7aV6g13^u>k98*LUP3jHhgr`kV*Zgj*Tl7i zQ{Eoaj#&5|)fpt5WUggPR?FEIm$su@Us&@P89 zw=s0oxx#A|_ma8sY!36ZhZn#~nI^h)rGbEqXzO&C*JeR)udq{3mt^`+1T z-_h%^2rE)tYw%o5%X@srpDLd0^;qkXL_|+JC+-tv`01jNnZyq4Pa(3WDl6dHos*Kf zuN+STNXMBZy^)3jeo6ycQ5Gw+YW8`Y?aFU{>Zqq$NquA7qUuY*u=dCn(RVlD988KK zp{)X6`Kxclnx(lqLa3ULg_C+3Y$SIxfE{tAjKVYH2q_=_K^zu5S+i~Pcy0Sz$g?DX zIo$v^Ds`k@?!f=hfdas-*rCA+p~&DZR*R;wP@ivq&H{*!(Ng!LPyQRJd$HlEYn7{<+mU&DedA9X)WMQrMzlDYF$&yp2bJV=3=?b1u zr>xcxY8*?UscXSQK+?63P3Wyi)g!+OP$^Mvr>kXocl7pvsg#wtspNx!xTzIfe+7Sz zil%VF7-fCq|90X}br?HZ@`Ky5?=`uF)t7oJh+29Q0tlEp`w=~uC8v@z-DMIBYCoHO zU5-0WjrT)4_o4*;!@nSkbq1VdU0Yc_@V&hQ`Vd05hUC~%FYauxpcLg)26L(PX7Ovw z*B1)C@52EuMQ%P4V56?hi|Kfcwd$gtruJm9K~o^52fsM*!n1-#lo*Fji08GMREpL0 zm-ggNen>NZUFXYiBE?JBig)3zeunEzS7XVmasKma>H^21A=~l?N2E`xHtnd2z(~;) zE#YbCE7|_-9x)r7bzF65TheMToZNzye{howf=J?ae55H_!gWkC33)S^_S1qVDH$Ni zxi~~XPxQ+7Q2M!Wj(7sLJbWwE|D5K@mVlJ(obc=IKTJXaB?Qbrm>;a?r?tHi{7wRx zYwx&?Yi{S@`aeY<@C})l1H#JgJnsu}{b!HSH zw>So=sdK3Nd4Hn$6i25Ew{}dY!lm78d~1R&qcK~uC-MI&`MwMOce)B`1OCH|3O$wI z=F^y+^7!k1r>MP`!M@{0QbyRn`tglwW6A)ih?sh>-T``HS>Tur$|pV^4}-j-S0)4y z35-mog7{hgCWaDO8$(q_c>?&XAck27cnI;o`ML;O{EW^fSfY4gLYUvWNc!6uWn5&+ zatY0F{*nrJ9xmI8&Aq6mwQEb{H(aR&yS2&&UR)C9B1nYtc6^hPb6D8`?o|k#ycVzb z4km)}ynwob+^7(p0_4UoMI|lBUgAiB2DI~8fV7~-D2JxPt@)wpKz-al=MAxATBFB#tyxJ%H7YfLW4%_yE92}dR zNc_sjaDDaO!P+)cV5N)3dUW{zY02Wz&)JU2XTz}|;Gs8jzz*p~C=Qn? z|5zd`)`%qh^~Pp){Wz@nf$2T$mG4U+!gzKRFgWI0``&iy@YW)r%TDeAl^$+rl-|2Q zBMdU^dV2AX!yF*uTn@}9!!sSE?luDyu-65u^(C!z+-D1Izvyj%;n^oJs%AQ6im-*n z)%Ib6-T^UG+L&-pOn)hwM6;R^nlkS$3lx`4^E(vLnR6$Ze5KuqQ z;`jIbtGxGSXJ;p0GaEabtCbUyj9ZiShEUX_P5J?)c`fSX^yh=;aelPW|8QD4)UV(7 z=lKg6o?3l2FZMJC!J6X_j!9s5(HWihN9@~L-7#(yXZL@ z$d&4|ki}gS>ryhul;=Tynf;@ObZC!k?oXnPyoRgOs{%vOQ^ldR4#g`I8 zC&cBwm|Hi7Fk&Z;T(vhNln7cG+`D#lkc08wI9QLve&`^Mm69;Nl z&1q7BL5|=&6J?pH#|S(HOb<_?pf1rc9rsqI%O&1M_U_u{?AU3vMQx|aENN@Z(af8o zbiW*F9ONiNpnq_-mu3BpOmUSW~Mlp}*_mk?hvsP8M(;mD=TqU(So(h$+)@sx9_5b2D8x(gi$P?Cw2+#9uo9W{I zcQuc(E%B}Tj;-;^!ts}d%(ME>1mflR2d#Er;V166QMZi3^S7O^Y3#PdKZYj`k&&y$%9oRe#(Zc z`kHX`I1(}lquaN#l(^McgCR*0cV930miG-DD5J8wARdyz z&>d_g7~ff2aL_j!rE&@a1jF4^t{(y#N_3bDP$$7tU7*?Py@^f8TQS+5VNOvmFORH6;$nN8%w zt|XmIs&xI#Dm(8TVOutPVy0BK-7BwZq`%MBum0uno~*L5M$XSvAqMi9)kuxmK1}_J zVGZRJa;_BW2PwSx^!44tPhf7D{k|XWQ7mQ~DZX$O)5?(q$owkWmC*LbmJ=a7>1+dT z>h3n9d3roynlY0IHzz&ly>bdFka;Wq&+4@$r;|YQVn>PP*@ZR_%M` z%c=GFB_nyb7Hyegu*NAPBNwW*LK6fVd!kCRkT_Tz!Cb*D5FZ0eA>~_+miu8BcUYXv z`0FL&j0A1cID=$Hhy1;OZ~nV}%fd6}89%DR`eI;Cn{?APB(9tqu0x!BQ@=PZ2O|ZA z6{TF+LwD@76s9#a%tgX?^`b;VqC}{Fs9G}9w)l1kUxBv&)0vV^c=9prS|;W;>c?6= z=YE_wx}&U4;k*%P<=|Gz17TfkMiTrX^Jt8!J5e2zDs73x`?5_nkjbA4xh4z zX9s&*NT*jvJ;e57W#i0UQz093Xun12Jie_8rCAzPn~kP{k2Wv9F!h_#-A<~fv|7wl z`!TY0#1u6CaVf?ndZmV)YUNRpR5lPOQU@C=-vPSlzYBb~>X6>ea^F#P_+}8hHk+@k zls@tyYf8&rf-sMzq|E!u;lG^$mwqSYfC1fYv z_WA64hbWK3UPt<&81I5Z%3FZ}J3|QfXFH|W(~no%OJ<}jcdm=$QhE4~gquVq-GZxs zGo9%?Sw*;vzS5keXt8M8kDIoL&7mk+{My#9!uOQF4~(`zrS^}!N}SZ>OnE4DC6owx zAm+~b5e17+k2$e5GcI#V^h_4FN$RxPhMblVu6WXJU1Gcer7sDAnc1w8a2zQ;FkD;qtl>It=-A!2wh5o@;DOVr+^B zly)|j951WV^L!iBo%9n3MdmeZ7vfcunDoV+?n&A2VE=7jiTY)8G$b9tm77&H#a4n) zEr>=Zt6~OQv=BXETXwt|)U_438=xc5_v^`#4LJtag1I>cSJRg#M*-jI;>>AZ?{q$U zqC!;(I?oI|1$LsO27L#1JcUw&Ka0mPh7=uuw<@@)Ajkwczy%T*~Xj)+sMA*YH6mXgH+{=!lDHII3JA! z4FkFeqXROG{uowI#PcM+zI#dJ$vgbjp4puS8mPP3|LLW$NQB09y@trR3A!^+W-n53 z>__RIO29||l>W*FruF7S#hTJhRQ~IS6e9^`Dl#gwtwCFvB&^!}j=Zf{S*>0-A-OO=%+ke@;A>NQa(#z#0NXDtpAhL&%@8`C>26VANyeLtre;yqAeGt&`eejF^V0fcNM}xP+c5 zFai1b$&~C5r2Ma`;_j=!K+G~6O#gz02kg*LFj#q*A2%ZsZzWU2J?x%T`wKtMiun4Z zwb0cJSDfHY&jhXyVv9RpTYKxplsL==%RQrdV9BQb8ihC{40jIbLk8nx>))*QZu#h( zRwr`|hi4TQQGl1?-?Qx+DBnG7rN0`m{q)YQ8@1At-xcjQAtvUyvg8iEn3}f%pqf58+uf zY%x(H6cv2h>BZbIib$=`Nv1guPFv${N(|z5QXYL_%F|ODhr6z&+O{SwfZ<1|guei@ zdVt+ZKrnQmzxRGI7QABt~YIq z>x~lMOsLS>^$}w|sPP0dXwN*e9O@huIML_s&jqr?3u9fvyg2SRi6_TQ>G40+e2N)D z$Q1{HIVF9Pll#`B-j-U~ZXiGb8m7Xy=zn3jgEEAJRq%_;gE)Z)5*du=lW`F&c&$O= z^v5*Yx>C8RX>y=Ya(oAXAHQ<6@#Mk!$bQ zNEhKKL8o^jnuJV*P1WoAqNDh|n7Po0_M3%|xi~bsS6oNZxH6?Cl>*^yu=G=8C!5sZ zS&m+bVu5a(uxe_9jX|5pi20$ogNq6&_EJxLY6WP>sMkuWXxD~A&T}VdM73%72G!e= zX?y|1sk{qOc)ChUjw)7Z;_KWk`JH>VP(Ip+?tBbRYlk3WmBdsXuUoE|RY?-#rPhX6 zi1@6bH7?fSl}$}JUB&boa^)yv%aaRW>jqop__mVdaVR59O#^iYWx}Q&TX+?r15_zO zgEBYZf8l0nF!L+YoDIWtv)Ozvl_qIiK6ppN85<@9m|_Sm}^dw9EJ=uGiWmJh-Obz z>ug~kEFYthI9&>3bn|9(+>We+kkrRzGNNQuoQ0$pvhrQk&hlP)urj|PVJNCX& zn7>T+S)r|^R**h)Ipdm}dXoLmv+m>7d2-wF!>+M5SZZg+j9rjw>Vrx}6zqX%x!(T% z?+MUW@syD_TKF;SZb_!RvsJD z8(2trEyakcTJg?1g;c-QAEjOWK&A*W34gYMhrNybg(oc$%bKf_E? zCga&Jf~N72J&&4>Ly=3nJr7k;S1L|VfBvh|!c{XfN&23#Woq&B7EZfdQQr6Fs6)J20o=ADx+|=5P40Kz zPs{~Vo&bF(a!0b$=CjbJ`KcS7Bw)CG@&pNf;a*Q`r_t&6l2|>Y#RI24(OzPc{X$^kgBi299Opbp7s_r#puatiVn3V z5YknSB!cE*eo+!HZ$p*G`4m*VGi$tlk+iWAt!T(Y6K>eTH_=bagwD(z`g0g|E$I`= zl-|7Apso<%LhWFmFZo3#uOemQ*?eSDIstu14|2A+O|Vdk{|m|8257qkf8UujA!nLv zSF1tjj>J*+Z^uXs=$KWQQ8WmEQTA7I(17$T8MSVcD z{W&zmU0^Dq3omq4V5x~!Im)BlYm%JmfM8b7j7O~)rWZ%!-Dp~bAKSkI~g6HzAsdR!G%Rk9NKy+e3trS)!pGtFVyXeA7mHgX%6JgInx||5>sCU-tglWIQojIHiEwmM`&+a{W!fj)8tTd+e-?NsFvk% z;A^Y#$W6)X@(RC8_N-2p`a!eD!p^$|U7ww1NA@`Vy{kny6#(3#eCR!Ij;}Y6vj=b# zHh?b=v!3{MQZD;G6jU^NBM{u4vSK5J@g`fp0Iq;_n503;<6U25DE!20>CrYnbKBt!v1@g7}`yZw3GP=BGSy^nP7UH>yIbma34@?qj5QG*hx+IZ|7`8q{{7 zVIj0M{KBco;)V05jOBy&jpvQzQH!N(rlBeHnvWn*Y&`1P_`&WMko!U*PL==_1EI&} z;pC{g%N8I{7k=GH!ll--K*LPJHGAMmYb>cT3T-*k#LsuWZ)0(O(+y11Ua)n5jH&_YOt1oB9D6#FD)pJHBRXE z9yVNE|6GI+qVCB1#ox5_Y}{P@`15`D4KlJV-Fbzod02o1uZ;jw=%vluR-nS4qC*nqmbAXORJ7WF&ayUq~ zAjzx>+h&Iz`Q7{iZL2-Fq-N)7cIGDyb0Mi1*nad9{ii1tYWD$fDOAW(RxT zSC}+sfv1CqCIa@vzeUB63DNmc-=TrYMVu2g4y`KpNJ~oVB5w*9oeWvGHvGu!xr;&Y z`|?hiM$WlIU<|s$mx1P-=_nH)8fM*HMDzT{@!h>YxM^Pzm@hZGtt7_%LdNW<&~xnt zRmmnG-B!gzPD_9Qoch>t*nkTr50`=p(1LHI|fXG=V!RN_-lwIKBOPL<+mx?ZpQi-mlNLlNP4j#Qbw#O}S* z3}!ggVCFDHFC2J6KYnofdDDN#bD-=MzXn!GGFgWhk71uXwM8J1DeTsc%@kNkZOAew zoqG@Uhd+A%w%yD2TxeF*bYT9Cuf?>TYi8Ci%zCu+tJj{hhL8 zc2VX>pW1cixRh}7pDt!B@XPfbvKo{kROdOYEb4nKQ`Sv13FoX*&dNuk_77he@q-|4K=a4GvklN6Ejy za}@%&f8n7_UI6vz_e$2|APT<9Rsk-Pq@ZeA(8Iwy+ldm@gsVG)Aq^WZcOz~WJ|LFl zW4~K{Zy04g%tWw6=+Q`Yn7GjNpoo$zJa}SBWH`M<6R>U`Loue+M9_%jxBP2W_Mn2n zp%Dok)}^Pz@DDG!;%sFmZ>PEA~~xyJSCWJ5hSZ`}|XB*t$TO7~$# z6LZ7-14^^|BbmkK_0G32GA}016S?t0oR~(Uf2t1USX}!1HbIQ#Jmm=_P)6*qqTe(K z^Mqaz9~$SOlu{mgTy|A8P7OG}HskVM0|*T2{I1sSLvn+SBxq%W6Tqxs4(XM2;T1AV zmO`-#wb2-4ha1iPJzrWt0$ZkdIybLQk-gyCbk6A{0Nws7t_J?S*gTR3SkbejQjoW3@gW>kc zQ;(1)Qykth8S3s7JA|9)63rW8>{NuTQt;9vKmh}HB&Fg!CS|p(cC%5?#b;A5jbW`5gb$typ~JB~G<78)|8Zr(0eIP~ zW2vZ)ua)PS|zT1_~TlGW^YIrjYe zJmcrbH_0DYD5J#Su_rRG>H2|ynS}r?A3!od#}>uyeS8qdR!jm^VE{JM5n0M=BgeHu zRnIF6>=IUpwFRw@0Z>vH;8n&NtbBdOsyE`m>>qwe;!wPxLq>uss=zO4LH!8xfh(6k z?B6nW4sD%wfF9mB*vezWZv4a4cj+T!lLnA7E?5LYSeRI{IUYMnCS*I39;nxuuws%x zfkmgV`&a@gh|xG5k~;Sr?kHHc_+1^|K|K!4EH~dS^pA|2JAhxf_f5XT+Mf ziV~f22qg34b9i~g-!AAxD~Wso@k@djD$iqa>!&h4Z_P2#Lv7%wW!pH3=p{DUv>}kgHDSm%2 zR-9QiB%R}QIoN)n#NZN@l^qzVa~JG~&Bcs0gfJVp7#$wPYm|a-%?zV>)?}p=hs^0Q z!4VmS`!%D>{{P59;Zvoa+`-XfknaUOdo=P@IS{F-ZZM;sI)=3G7)k)YBglv~& zhlt?IJwh@q{!KAgp^D6IL*g3gbvV&VrST^6hOiurf6x_1|m!V4QtZ#`HpsXsip3(l8ITxy?{E=QZ8rgDh9 z1rcYJ^q2D2@Al-My}^ujRH#oF=|r+D`|T%!n~aKT4 z-cXIH#R5hoQH~I!K82!sg9mC2D&{!^@CGtuU#CUu(X3V@_$Ig0(-DG{V)v;{Fw&@# zCEE=K%f<-Q@Lp7D05T=GhgZqU^9OWoG8eCxzKF;9AYAEOnfo3R3_2U?B>DJxF1NLR zuKfH?T#91YLkCm^m_45Bb(#@_bCUF1`u>7``v>SX2T?c?PUh2|$&(YSitzCE=69{k zLk|QqB)eKWrVE@qBUrs*a>prj-pQAQR#V-3H!5xH}nZVNhg zE27PEzy;o~cOF!u9epVX843V8G2lxQ->M(b^6EDpJ&&@2^V#c(Ag5rShyDe@o8aNiMXpYpk%LhT0| zz?OISX4VQh|HoOP+F_OM%}#EcHOgV#J>sXHWKee|YLadd+f$rfYq0l%HR2ft zT)T!R2G%&#ED;zs2D!VM`xkyu+Xq3VV;NN%RGu4%QASs~(Kz-4i#;lJby`PuDO@|R zB&o&X#W1(xTQi6BzF2lxjhMR|HulCe$e;!6l+G`U!WTvf^mxgGtFXmhi}A_1 zs{<|KR8IZ>ep&^-e53vGn5W+2nQ~SY)&SiWCd@whPp^x%!#${Bmh3JLR0iGmyI#P| zx%K=UIHR{nS9I4zf`a02NqMcFVwx{3bUIt7vMHfeQab9sZ>@{*aKz&;(IvtiSg#^| zko!u!Lwv1+iPW!kzteI!r{`vX{mmLi{$^>|pW)ixZz_0-$z2Y!dK?}v6mCgf zcJ#F{rr|Cb%UeMI!h+#``H#v3L|j1^^++da(=KgVS1etdhIloM`SPa`Krmx%v@Ye_ z=%5endPBtOdMsK@%4cvc@lghjm2*Gqk4}ZSo-o!p2kx$0jTWW6mQ4!>$W`Wd0a%<> zlfg40w#%!xPinWwrvVHs^Gw2?K>82Z*kfH zD|;Xnz%^Wz&eZVU4U_Tbi7;@^g9qlGcZ2bH2wq9>%@LxAtT7eb%X$@EwYiN?|| z7=HiZ8BDO7N>xzz1~=sHQHo)AS?t@TAX&{-lV(r|t-K8d3b{6SL;DKW|6r|&3D|$T zK7Is?xX*6Sg9&{-tzEf4cd`C(BHIKf1uzFu_eS z7rxt;RT^JPS^o~4q$FCgi5DSd`=i~+6;}a-)+8YGQOAFr2SUl_z;HVPCG4=vS*@tv z^^6kk9nKAP$ycP1r5uYU;4eaZ5HH_B2d6jy+_dtx9aE2Rl@i9a4+71|NRqYRH{ z)Q7(wQ~-fnsoAb-ixKOK0Wv%MzZh`1Y5YT*Y@s&9B;KOA{mEeACWRgLO}z_f-Eq66 z0(ix%5R_bzj3i)|-`e+fl2+?%0ks_TZW7R^*|!M>@HY=JIY@??ZAKWNmDAyX$BxS! z!py>I;Q!nR{%;XF_R&?lQX(kd)W1KFRKxAlpDi%~5TJSA!Dc_3=xNpV=e(+zzDFn1=#YN#o7v@hQhw*V5f% zUkxt2Sj`zZ#(p7$t~{NZ(bWG0<=@I8TEO1KdXz3>Ci6sbA!5`$ZXw~tD;rXBcVGXc zP$cS4XV5(}gBB7o`ja{R`tzRo|I;L*4!q_Sl1WurL~Ss-4AuC_AVogjyI1G783K+na;#?-e9T>aNrVy%2@=t}P#lA@fuE<$cz03QmHYUi; za2kBTiM77f1zU4O){c%m{clgHa$j--iC}C&}85<`c}pz;I+q_)aLc1rWb}# z_NAo4sN8S7HoA%+U!{oqakJNev{WqDNAX#!Z=mIN@`6PO#xc-7qB7#kjwgIzJuwdZ+Zg&$2i3=7#&Fk#{st?$K_Eiu4jZh%$ajiW) zBWSto+@el_e_{qQM=wBzpLKsozGigf-0xV0UiuZ$%S5IbD!MAg4CtRX?jY`%_IY;l zOj;Mb>l!lUDJ?TyRe?X&eWv+KS|dq(aayVnBDa@+^9*$iT3`>?i^1>AC1sgf6| z-@@B%fxLdep3D%7bF`j%Cf!v6?DR!*6-J*1LzpGe_i-t|1I8Ck$gBv_A^P3PLUy7_#;**d&nl}T$7?hB9hSE0?7>aco%L;t%+0V$Za zbxm4@P2xs*P&_GI2MKGh%v%@;>V4Mf)Jqd>)7*7}!N-zPODDawF zdgJ-SJ#;xH`F$c<7y9KpN#(&5S+ZKD&C}p=?S; zLK3pdmr-`&*0QLhO^jHJ;e7jW{)xr;2;Y-LbhWJ><~=OXP~u54=U7 zyZvW$T(N-)>a^<5GUii`y=j4 z?9JHu$y?CElA}D%UVWQjxxMhHTpkUU-r|z@qYPjYpA2?x&S35g`_%1}k{;1vE|3%_<9p4Q-CZ>$U5lK1LMpzI=^gmbvO1YYiMM-y{p1jqaB&}5%7Yl?dcMM z$FRtiL?qp8lgGMjn=Q7z<{>Tj&wX2QiC{YK7oP7Z?J{nMiTxu0nVn0m#hFT^U=yew7?Q6D-GLtTfF>u37xlS*FiRJ_!RfjAL&zt z@6m-ikwZTS;XqqszMs%<%GDO#?VjS&nS^D1)>{d-;4xx1{L?Ua+zwo?J{R;nl9_by z)ML~~A@HzBD&l~ajOgZ?UPh*j&YP)=0*RJ>ns8+iK2}-{HEp%L^=8^|HUSK~AM5_~Z_u*K7e}*>PA)^Qfz5z6JOGE}sRh z1WGn#fAeO&v8QRw4eqg~MD?9LXl$9S5(`=y&tLOnshFB);PH)F8!Vns4v4A4E#Y`<0bV(B%O= zwVR-I-_47M0q)lZZnJhEqign2ZW)_V_h<~eWAB*H{G`L9yeDWXoG9Bck){v1xN)}B zqKT0*?RvHmy z^5eEkUz7Xc1H{(|o*%8>yL!cPkWGZL0pKOCisZfOd9##t>@|s}vil`UL`Uy9ap=V{ zSP$H6h48L-Wd*M+9De66VqITcX;s=u0ZA@T1Li3B#Wg>uYjx_^eP6Y@*vLgc)kG(IhXpd@dCeG-m!t7OLL* zGK$N+z54uVK8HCj;!Qe~nei#fN@sj- z#StInxuH2-A&nfrE9d1vdui|}w;&AXV#6>_^L%a;C=TFABd2t6z7QbN_?s-8gQMLf zAdU3)E@$w&HZ#eSmSo75IeHSC@e0N70-=2m-#k1e2cu1?l(_v2Dmw-$j<8zWGLIFr3WB+Gu=NhHB{FV3kA041%w{o zG@En%s>x$qGs+e`?Kl>rxMm-&`W3d$W#|Izdc5I2+BV|)76ym%Ndo|?ETo9D1BHq| z(VnDh&_(v0t?i|O@?~68G{~AQ@;^M%U?#n#;`>fcs8$Q)_nd)ctL^T;0vTja0P(G? zaT*$aL^p%i{ym}B;g7+?F{f8gIBm2j*B4|5($CcrOn%_8aKKj_^VQr`YrS3)t-&mU ztA{{e0*yDI;Rb({E{oPK%a;IjzO`!`e+X1TFR2zWj<3X*wcMqAGe}bA#CXrLTm7zieQ<@}3uI2~SzpoL~K9$al2Cfx>SfN_8pqPM|HRKv< zMiuUDCb}7}i3GtM&Q%jIYE^0!^ZNoLh`Na?|4!*Zc5z*ObXYrZ)GpX0rvNKOut~DS zx-6Y+ydsLWU3a|h7utuR@$n_ISQ=h`^L-}hTHSFMo@Qz_cZv_e7*AZ1sazhYwRI@h z@^@?x!(8IO?W{nFAQo$%Vo#d#w(U4o!#*QKSgp2n${^8H?6h&$$*Zb%kB*tLPHZ%_m6XGEYVN4V z6?a=v&{ZFe;dnZW_kv?sKJm%Vq3+i&M7+FDIG)T2+^rXD(eJ4f$@ab@gspQq$sPb? zHA}T4o8j|&MrOuxUnko_HGd+fuo`3UMshS^-A^E>O$bX~JFr-=$H2Wy&inDmp!q8b zHH`+RKS=MdORT68fj^vR8tHm9K$mEi?6U^9^E+#>;zvF~dCNXc_}%%A?p^%XI?W=_ z6e8^49d>g}Y?{0oEVGD7fulO9M~`~f4+H&f%YF&XJ>8{y^(7;YzSg($#?_w1!Y)F8 zKpaU1s)3-dG}uO2b) zS#LQ6L?Gj!X?omNOwJf}Ae@bR1A0m5pk6o=)Zwn=k56WO+LH$Mt-KspE>G2ei1mQZ3&MUizHh3x&j z-9PjLyUU_ACYP3P&d8)D5>hZ8De`DF`PlO~;8WIV(cIj-q7QSLxx;f-Ty5HL;Y9`d zum7`R_i3g(r_;L0Y%31^S3J7BT{N2!j16!nhFYW(ylOuwBuiH2Ky^`Py*HpO&ca~f zn`-5Mhk!^>@oSm3gX=b^y(P^p&}p?@_UOF*c%tI4cw}NX_U!FtC{Zw+(_}kre5S-c zn64qtB1Mh$-Ij?Hv>ZvcB<_AEbMK+cH7}8D~EUXd&xbb+#t0* zd}UKQkD)0w>OTClpr5d&(07IC$@oW5!w2-z%etoUixRKFkPGLV0W)0J_dJL-6EJjI zl0~rsbzXi*SlQ`lXU$eWlmSX+zYr9`+CV0!*z2ZT#O4U282P3?OSlx%dkyd~H;NjmtM2 ze73Z=%v%&Cbi;i9^mC*cLp02T@5fO1zvFwgXR|~B-@~V;IrHe^&9o451cS)X4Y9&a z_>k+hg|x-pLZAIzSy52=v)H<4V+^wG;7Z8gsa1CvG-LY{k#J-Baw?QaK5raMQPRcbuhXr^Y8mbh_TrC%?Kd~$E>7oK|p+r?l++|LRDHy-la<$wqYtR~_$tSi`DFw>6 z4JzPc^C5wu*!#w89m@R~J)~4ij63}o7FKIg3b^tqZT@~Ob;bUWyB-;gj~*GY^pMKy zf6=(3CIQC!{yU}Y+Db-x^q;*rXh*1xRztyW4+!SI`XOvMr8nM7|0oRrBf~QVUJ0@? zGd*$_o$oXA*Pn@Fw+CMvVZB#E{Gq8PKt0a^RJgFD@LLmgAJ)GDm#t=M4u5tfY?O6P zbaz(@`@%E<V8EF z-+XHPk)x-I1&y&EPkR;o=RAzF*<^d?$whcuQyX_tdw|H|rPzP4V78ImyHGoGs7klPsTo^3e8t?8iC$Jn z@Qh>hFanblMGp}vr*>lfOxMdf`B9&n3w4h1JL1juG`Hg`NkxC6B^)Z-EWB-|{x~9h z0wI!LZ@L5g)V}q&6fTnb>Ty`!IgrMuziX(VV$Kr|ACR%=j!SS2R>*x7CjIv@c9shd zTb%5p7Gf~Y6t!vd6r^8PP&Qg;>gIz}B+|MGXc(O9%w=~0+!=u=YbMX#zjKQ3|)s5HRNCxr@n`8J<(4KjBt=jKCP8ZM4{jLD! zgKbF~fA6Q}uc{@AO2`d8&dsCS6cLz@)uG($6lMQZdmc?TgU!w53{MDY?S#o7F_F3r?Zx}JK_|nA571J;_i=7lAj_B%% zSqB-J^0;Rh9XZ1m6M{lQwFp@uj}aHb-Vu$YDm381jJIhL8o0!xl)HIQRI7ZWiid-~ z9UR!qTW*+KG-cjm6D32D_3EwFW&rv{c{gH>Lh+jD`+k^4ffQ{Ye;9s-=SQ6!D2gs! zOFtL!?Vw886gPivPoaZ<L2`xU)$WN0&yUmjKb;wCHO&=V zU7fZOHsx*v_WG#T9mI!q%)j(9pvYJ9<r|O~=K9+$Y zRI+t@h6522j;pm^li?SW8!{&yH z{Z%x#{s)ykWQj%aLV7}(-ltXpe;eTu|Ce&+UJ$LVV7^p`xW$VuzRBTL0uoV(h{wlP z3(Lx$tI%?rB>o@bp8v10ua1lQdE!3e4y5Fy5fDz|NZ~+AL>i?8q>hp}N?J+;l#&*Z z?(Qy0K~S0lq+0=%Mnn{(pFMxy*Yo`Q@YnJBe0Fv|yR*A9v+tQWK-c>z`1`%q=ex;Oog+|(xS+plQI-S64c!d zt{{c=*Pti(-W&5Am2w`cO{_ukz=%vXD!|}MlgcB_FguW;U5rp= zCH3t?iY4wK=EdWko%!7|yq4biUMv^@w%xC-Rsed=#C{EBCRp0dt&@u=a0~$@1-aq49YYAJLKR7Ai)P}HTE8uLa3kKJ;{qts3 zK#wvS4LBjo6LrZ6{0%^W_N<2k(qCJRA{J?x9Hq>;;Xq?C4LF~v%s&N*h?bOg=Kz(% zpxiJa@&0H{hgPLi0%$5;9ben4A2<%>p{H#|eMhASb9Q@I)4)+W$0856gk%W0M;?*_ zXk7QMnhrmgg(oS_Qi5qbEJO~1ms(h``5Hw{M6R8TY#ob7f51v&Rv@IC8Pj%z0zhPN z>~q#Z_5qW|58%?Gj(RtWA&fqVqJ?aK7sR!BcA~moRp#JKFKh9v&)UE%_^A~%Dxh_L z#282)MEM0AZi**YpsBPayxe)qxYOO?s>H7=S|KYT;9HDAs?=8dAC%yRYkyfjCYQAy z=44yN+gwusq78^QDQ3kUUXA{3hPw&In-ycyD0)K7Di-p+V7>4nK){QY4=udEaSR{(T;e%bXmHV9x8=G;Ow%Myyh0rabHF(u%Wy=<>X z3w53$QO2odBR1V*KQzxKDWqMt<2y#=t3+kvq% z$-n`>a|@V^FFw8KNZAY`#fa}1$D*feLD^c+F|pmXW{}nc5PN}Q`LnKi@cAx%PXO7-B=_ zW_j{JmrHg=*^8+zj#YjPsN1ct+T3X2ABnj(6Bno#B#sh$Rrk`3W5Og|UPfr%%zKSq zPi&MYZa3ZXL-u4k;>m_2IfIeAwM9|)`xVHn`AVzgz8AU>v0;&9%HOZ)4y47?1qw2p zbgSdzDT3uJ0@v!MQAH-W$N&1mp|{Q2@}$G2hw(k7TS0t|>8Mkneg2n;opTHhF*A4L zEGt3?K&cPW%CM4LwY_k+UY zMnhj$0jj4|-|7=~V}>K_-I*DfmP@kT5!5TNK26HlhXO%N3@nc9Ah0Gk<_CSqU#DzG z%$Fy!y|OWrap9M0qWHd00EOw-gnJF5WDAXdJQCMN7R3q#jXG7_E3d73P$f?)VrrJ$ z!9q-r*Y{-s#r;(#55)Yi6|}K-Cx89420z-b;tA}A^4rR;?fHJImOlULd`bjTk+y1R zeTe^t=Jv>o@GSk5D;IWgX5Xj&dI3gowb^#L>J8q2H1Tb|BY>hf&r6G+@er^k+g~EQ zTQtxsW}RWl)NK-O{^pl^X@+Mz_MuI2C^cAMS8k>yF!L5I_9a|4e~3u01#$)REmoU< zImeHXBUkZhFwrL!4PYd}b_2)GxGqQu0E&MoCfzJlq)!hUQdCHhW^(Uxi>g#gAw`w` z%IbGd!;!5B*fuVu(qA;DiQo-(PnXPv1>GPP$v3NrL9IxE4Sbvk!!++fgmkkc@iZ}< z}Ap3i+t{oXJ!gIt)VM|HE6tkVcd4~^U*f$u0hcspc8E@KQaY^xRktcLr= z58`y7%%VOP?_Mva_^t3HM<)CsOYC%IUe7OPSI<7QTj9XeP$N|4y0jY}YanZYx{vT0 z;}dvFH2h3tY=|upC?>?4_huhMne{NsQ4EGoYRUD+p5dqSB=JS zqv3>f#J5g$^d>pqQZO`hT-+VC=75L!R27fH5b%U zsXZ*{DOl^L(B99HE@Rxu*eZG7BZ)q%z2g?W9BhqEnnjqvri>pCri4O}% zP!hbLqZ^RB8n%)$mne914&Zd;4kN1)`PsUgh6(D124oaCD zWY2^aq`JM(58%+`AMt)L539HfcFtO__j|=(0Q`$Avn>CW95`imlDx(B%|&IhSeP~` z)E6L`EPssvM*egFRCfpIdDvjSW)&N{R##B+g+1$tG6_L0T}iwt zG5#U7P#y@^t)F-NKyCxol;tG5>tEzkbuL@cV8fha+Hw0dWedQqo_K3K{W?^#irtBM z1trK^h@dsLGf-;9(*R*oROQ=G{_*zuF&>!Yj{2~*1vo~$A4E2^!+v1*|C%Wms%Eu% zxZgNZ^wB8lx*Jh=K{ztGlPW(ooVA0QrKeNPTTQ9Lov;f5v|wjr0}7S<=6K`B@7&## zL-H@b)@HZgyYc4i@4`jb9aI8H<_(_H1G$MWo6WoB!xHrL0oN4^U2#mD&fOKCty@TxZ4&gGiZv-w zDWK>1YJgnT&{;8}F&9T&@h!9=jQ@v~+=;@LPfTm=_(R@tpT+4>C$T(na~T1^<&U5x zorRDkU}`%te<>zl_GShCNYBWIm!cF&taSPXP>~*5l#Wzl2Z9bWd(wKkAv(HhUCWhp z4XiJz0WMRzj-h`Xm8&1qiO@HF@1Px0fk0q`;fOC-?&Zel4_{5adgg@AL zX!JZY=3?r-cv*$7AxNEOZ9h?!m()5GiuU1`r~iOr-cOKu5wK z?2xJE({f{|jx}DvOX|?9x`)QN?3kZZJo!6Lwd?0}=TJ6FP}fkJplYvvtl+kru@V3Z zkIa;~)u+U)0NArV>g}#cDNpxgGur(T}u#_l=5Eqw1o==c4 z)M{2hWHN%BZMPQNjxcYMQ3=L+OM(dFHoVH}TUSu@f@Z8^w48i1&Bg|L0J<4WnQX#B z|Ae%u`vIjZ>}&3s)KDhzfNLglYKNvBI~3##j-pu8I6c)Q2w|K$ysZjEzJFKbHu-MPJ zD0~ECUufM1_{_%!>RjRXPA1+TXP?&>QZO8i7#QNdBi50!mlT+j5yndE6G-6!=9sOf zH$v$W&b==4xfOlRy;k!3*0arW_u0}2Z*ODAX$+A?K&qzrBF)fZ2Z!T7q@rlIMmA^u zqgENzMK0&O&4l>&+;~iBY5A@Q!BAd-RB1v=+p5NkbYEu0Ox;ovW<+$#Hp!R*6ZeU4 zVR?+hItg;;4Co}oH5Fr;MsU9T6E4~Rx$D~4BIHO=MyqjD7Q;tN%muCZF|j%*mnz1| zh-n-_C=4nJ z2F}Q00n|oc{`>|fr1-e%)3i093ku?&L5ah61cfRG9| zo~}ZB34ylYKrAGH(hb1kKDgvsN0NeTtl-~pvT12cFCLIPOqt4R$)P0 zy#^Q5R3^pQj?_O$lpl{iIG@B=MmVk z$)E{eSHo6*FC!QFDjOfO1L|qLG)9Zs2-oJCgs@3j*&?pa?}4)Gt!hd{Pgrp08zqXG zc14b8CVL}@CIH9Iyjkk(heg^tZfcCAhRL065BeBjI)ZN^qacge%UaR`g(Fl`!2cCd zn8t4$3Y5kh4s1q%@z4NX7y-7`bGkgRjw3-zwhITF3xR=*CS%5XVhn$Q1x841C9fzW zAZ&C$8C*civUYaT2(zf`#4lg0I&|Wk*ko|B3i#jaJ-M{j2EAY zZ!%(OVj0{}ghcaVmva=B-DbhdZOfc;r1ggV(jVSDAo9Qe)cslAQ}D4ebJzaK)M+rW zFmZpV_{S(*GztuNqQIc3%))o2o=kxe57JEF-&Y2!rN1Mo68}xdTKS_19G^H7|R6>kzl4VH@YwuZ|G;p4nHDo0%hb~K`c0C zYmsk!HPyU1cn3y;x|1{DIlV0{{f`xeQt|YJ9ATE_X1V_>?LlK!_p#p->sBAt6#6%RA3fE-0NKfyQoRe+q1ZC6&HVa02# zDnW3_QtN@W7~pdO3+N1Zm(l^@x~h_#C$0@fmUxb$$XHy!YMdA`P47hxGBnRs-7FZ= zfzq_A4WfCRMj~3U6*RyfAtzge*E&A(oLRF`;U9K?6NZ>B;eb19!=TEq(a)_-IKORd zb1%l;c6mk3eGK;^DT(0r_SRHMCIQ7IX(Sp~OioLOU!EDu8k$%rreLkUN=H!T*X?#9vU@Ff;1F?)bSRxJ;KaU=rfFV$0 z9?+DNG0n!@c2s;)L^s$Oac=}u#>vZCU-5IcX{R9Oji#gfcf9?3w2Q*Z zB#;A%`?isk#=^9lNKe2jhg%u{b-EGjI;*T7MQSSce}&=SBRk-J8zhD+hxWB%Y`|st ze;H74t?ZK9Z=nNkew!ZiqqgLW-|zY?8T=nTpkLI^1Y(i&$3iGcRK(E5$c+K+QCSAbl*SXB@&1btV zg9UbF|6aJ5IsC!V^7_D-;OC6Gwsj^M#VAQIGyOCyuVR^WvC-Dba#5`X?}+H<#H|Gor7A}p6!%k z#Gb}dGF@d#bW6a|SCVh%}IiZ4e`Gv9lTSWF?|iU?mg4_|q zIG?>U8*ahZ%v~nP6?3-y+tWLC-7?_QvoM_`u533-LPgK#4z!llL=e7gnYF-(ys)F5 zHre}cKBQ6)F3vA}nRVgxGbxoTajv4MDNM_KU~(H}%BFornZK?0rhVY%b<>!Jxo7kZZcqLAAdKZQgU-eG(rM&8mh8;rhUS7=No@( zuo5gb)V&BY@Z|cKV9lZOWC~u;xkaz*KU_${r@J536o8lGm(Ck<}7i`B4|`E%%26}VWcQfne@&#J({>mZtKYS0ny74@=rLZ>jZx5OAd9g=(6g2F8tKIrS|PP;Eq zwzc>pf+U7zKkJ0=#ut7W6Cmu&GvR!f>X6Pj+%8d~L9wXkK~Q60C!y3z-Cq%IRXy>! z-1H&qM^TkZd5?wI2wI3}kK#g+_SEou%j!hEuxyxEF^w2^vrq~B^3xDi5l({Cisutr ziYpb3Ym&PSwcL(!hQB3usV@p3NUkhnXX^+~LR(VY%vB{D`7MyfECXQ!aJ$lsh zb<03B#Fa+@Lr!1b=UL(QsCBcM>%PB-e~T1Kz0f#zI+_ z6oQ>mUGQA#63YiR9DDa%GpU)rU=@y!R8CY!w{}Q`(e%jEnwKC*SrI=eMO&UU%n7-4i>(RpWK?+ze5W#(-jG&T=y=RyG^8J zotZ@%HzvVjSW9E%8Vms>^R&> z%m(}0>{gp{TObg9{U(JeVTUbstq_Wjp7s>KnU}H-UwUPo-6VaFOwn_8P%&5AYo@EH z3g=0_k=K6*wvG?s({G<|ljNQ*14U6o19vssl}`_2s#&t!*>^`IOH`cSzverYGLvhs z4W|*Zyz8F!J#P>SF}wZx;`KCxpPH5lK{_vQpAoAK45jOEV9ZLuyBhUcQH~DcKP)Wd z@=UG1wMe3^SJK&lu04OPGL>KTsO-k?hrin2+ZzRf@+hqZdif$UUhiA^-SHsHhz{F( zc6$o0nhMj?Bv9TWzL>1`li|@cFDn~npJ(g)6Fjcx9*~1E+gsY3_>fNZYtx-^G_IHE z^_7D)leTrHGsG{-{!R|8AsvpK+1$mXx4~$IM4GRf0RgFAX2zG8&yfRd^#@sQf9Eb= z(xJV@{9Rusj@kKWk(hYq9rYd}AP~_9B=$vOatwlG$-mF$hC+|H_7?9Raa&!M_y20c zTPRWcI63HqK{rXvC~g2A8tHYX)f|JjF@3LMM@_?=S#9BR%ebW&Y28G!>p2n&h3MU1%2NUm zh@gHxRl0ILOZwevC0+9CJ5gIH-|}ft;XDk}y-{-lWhM}a+1n}I+lt+?OZbIlA%FDG zR_2Co-N^W}(Z`F4L$y7&{$9@0{>HG5aojiAJpGNj5(HAS###2Hw{fhb%$&dQsKV-U z(>v6-M2_>=>U5jPD(hT&IV5As8dHMnUrX1mDeA1Tj8RVsew6LOB7JW!mXrt;KFvWZ z`x74m!9HrQYMZ-A(DN!UTM5aK=x^Gz;8(p}scIb5)d~TVV_UI3ebGJ4*uax{7d+Ia zK@FIn4z%}w9mTWJ+y@jY;wKKdWXWgZX?kvTZ2BIr%JucqFn#tuLn1C6Yo-Wq0V;*FVG@9L#`2Z2B`(wLA^WKUK*WmX(LFCw@DB9lu!o6fFBzGUI+`IeC^H8}F(_nkmG2*m#n zq6FNxrQ*WWG=Qr0!be)jhcFVAzZ^9&1Z#Z%PM9iKK48KCHBw~kCvr> zl49ZE58dj+1$8oO>~rwhW>o5pcO4(zR^Jaj&%3mJv$yTc7yAHKZ}sb|kJegtwWFZ0 z)*p*$QozLmCco(}f5~0Dr0e9EG`)5UYr*Y865}n|HeHfu1zTXgA=dUzJ`t$X%~mov zi+ej}9}%&7>wszgR7jN+^CWb)&#Rn$*Ocbj*!B6mbuybFVgMUOcT5S!p3L9YlVa~vyPFw};J!?1 zH9yQhvPeV)B$8uBP-mnnmFjmYHWkq4f^x_i pQ$b=J;WS8uApo!Z|NN;Q-Y_kW6Bnesbp!YM14RvmDmk;j{{z=te$W5_ diff --git a/assets/lotties/ai.json b/assets/lotties/ai.json new file mode 100644 index 0000000..7363e98 --- /dev/null +++ b/assets/lotties/ai.json @@ -0,0 +1 @@ +{"nm":"Flow 1","ddd":0,"h":319.05,"w":316.81,"meta":{"g":"LottieFiles Figma v91"},"layers":[{"ty":4,"nm":"Ellipse 29","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":28,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[8.5,8.5]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[208.5,193.5]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":360},{"s":[60],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[4.69,0],[0,4.69],[-4.69,0],[0,-4.69]],"o":[[0,4.69],[-4.69,0],[0,-4.69],[4.69,0],[0,0]],"v":[[17,8.5],[8.5,17],[0,8.5],[8.5,0],[17,8.5]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.764,0.8421,1]},"r":1,"o":{"a":0,"k":100}}],"ind":1},{"ty":4,"nm":"Ellipse 26","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[44.5,16.86]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[153,163.86],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[153,161.62],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[153,161.62],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[153,161.62],"t":360},{"s":[153,163.86],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":360},{"s":[60],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[13.81,0],[0,13.81],[-13.81,0],[0,-13.81]],"o":[[0,13.81],[-13.81,0],[0,-13.81],[13.81,0],[0,0]],"v":[[89,11.23],[55,33.23],[0,23.23],[47.5,0.23],[89,11.23]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}}],"ind":2},{"ty":4,"nm":"Ellipse 27","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[25,25]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[181,147]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":360},{"s":[60],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[13.81,0],[0,13.81],[-13.81,0],[0,-13.81]],"o":[[0,13.81],[-13.81,0],[0,-13.81],[13.81,0],[0,0]],"v":[[50,25],[25,50],[0,25],[25,0],[50,25]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.9202,0.8912,1]},"r":1,"o":{"a":0,"k":100}}],"ind":3},{"ty":4,"nm":"Ellipse 30","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":2,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[25,25]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[184,210]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":360},{"s":[60],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[13.81,0],[0,13.81],[-13.81,0],[0,-13.81]],"o":[[0,13.81],[-13.81,0],[0,-13.81],[13.81,0],[0,0]],"v":[[50,25],[25,50],[0,25],[25,0],[50,25]]}}}],"ind":4},{"ty":4,"nm":"Ellipse 25","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[25,25]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[113,160]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[60],"t":360},{"s":[60],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[13.81,0],[0,13.81],[-13.81,0],[0,-13.81]],"o":[[0,13.81],[-13.81,0],[0,-13.81],[13.81,0],[0,0]],"v":[[50,25],[25,50],[0,25],[25,0],[50,25]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}}],"ind":5},{"ty":4,"nm":"Group 9","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.26,157.25],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.65,167.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.84,165.6],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.53,158.85],"t":360},{"s":[157.26,157.25],"t":480}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.51,159.51],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.59,159.52],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.57,159.52],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.52,159.52],"t":360},{"s":[159.51,159.51],"t":480}]},"r":{"a":0,"k":-0.83},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":6},{"ty":4,"nm":"Mask group","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.26,157.25],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.65,167.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.84,165.6],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.53,150.04],"t":360},{"s":[157.26,157.25],"t":480}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.25,157.25],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.65,167.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.84,165.6],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.47,158.85],"t":360},{"s":[157.25,157.25],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":7,"parent":6},{"ty":0,"nm":"LooperGroup","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"td":1,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.25,157.26],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.99,166.65],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[165.6,166.84],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.04,157.53],"t":360},{"s":[157.25,157.26],"t":480}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.15,156.14],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[165.47,166.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[165.68,164.41],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.48,148.92],"t":360},{"s":[156.15,156.14],"t":480}]},"r":{"a":0,"k":90.41},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":316.81,"h":319.05,"refId":"1","ind":8,"parent":7},{"ty":4,"nm":"Rectangle 44","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"tt":1,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[156.88,163.81]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.4,150.7],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.72,161.44],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.93,159.04],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.73,143.49],"t":360},{"s":[161.4,150.7],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[313.76,0],[313.76,327.62],[0,327.62],[0,0]]}}},{"ty":"gf","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":360},{"s":[156.8820037841797,327.6239929199219],"t":480}]},"g":{"p":3,"k":{"a":0,"k":[0,0.5887843137254901,0.451686274509804,1,0.7067307829856873,0.5048627450980393,0.6451372549019607,1,1,0.3122745098039216,0.5873725490196079,1]}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":360},{"s":[157.11399841308594,33.78219985961914],"t":480}]},"r":1,"o":{"a":0,"k":100}}],"ind":9,"parent":7},{"ty":0,"nm":"Mask group","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.26,157.25],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.42,149.72],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.58,165.48],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[153.7,158.85],"t":360},{"s":[157.26,157.25],"t":480}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.25,157.25],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.52,167.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.84,165.6],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.53,158.85],"t":360},{"s":[157.25,157.25],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[20],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[20],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[20],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[20],"t":360},{"s":[20],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"w":316.81,"h":319.05,"refId":"2","ind":10,"parent":6},{"ty":4,"nm":"Ellipse 13","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[68.34,68.21]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[154.7,157.09],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[164.02,167.82],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[164.23,165.43],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[154.97,158.68],"t":360},{"s":[154.7,157.09],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"ef":[{"ty":29,"nm":"","en":1,"ef":[{"ty":0,"nm":"sigma","v":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[30],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[30],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[30],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[30],"t":360},{"s":[30],"t":480}]}},{"ty":0,"nm":"","v":{"a":0,"k":1}},{"ty":0,"nm":"","v":{"a":0,"k":0}}]}],"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[37.75,0],[0,37.67],[-37.75,0],[0,-37.67]],"o":[[0,37.67],[-37.75,0],[0,-37.67],[37.75,0],[0,0]],"v":[[136.69,68.21],[68.34,136.42],[0,68.21],[68.34,0],[136.69,68.21]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.851,0.9007,1]},"r":1,"o":{"a":0,"k":20}}],"ind":11,"parent":6}],"v":"5.7.0","fr":60,"op":480.24,"ip":0,"assets":[{"nm":"[Asset] LooperGroup","id":"1","layers":[{"ty":4,"nm":"Ellipse 3","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[118.85,119.08]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.03,156.76],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.15,166.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.78,167.02],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.5,157.01],"t":360},{"s":[158.03,156.76],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-51.02],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-126.02],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-181.14],"t":360},{"s":[-359.99],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[65.36386305921113,0],[0,65.4938515227654],[-65.36386305921113,0],[0,-65.4938515227654]],"o":[[0,65.4938515227654],[-65.36386305921113,0],[0,-65.4938515227654],[65.36386305921113,0],[0,0]],"v":[[236.70999219690856,118.58001884340739],[118.35001713265146,237.1699956998181],[0,118.58001884340739],[118.35001713265146,0],[236.70999219690856,118.58001884340739]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":1},{"ty":4,"nm":"Ellipse 3_1","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[114.72,120.64]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.11,156.75],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.2,166.73],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.72,166.96],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.42,157.02],"t":360},{"s":[158.11,156.75],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-4.11],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-55.13],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-130.13],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-185.25],"t":360},{"s":[-364.1],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":95.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[63.08385213008413,0],[0,66.35383995118667],[-63.08385213008413,0],[0,-66.35383995118667]],"o":[[0,66.35383995118667],[-63.08385213008413,0],[0,-66.35383995118667],[63.08385213008413,0],[0,0]],"v":[[228.44000998621374,120.13998576783972],[114.22000499310687,240.2700129824825],[0,120.13998576783972],[114.22000499310687,0],[228.44000998621374,120.13998576783972]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":2},{"ty":4,"nm":"Ellipse 3_2","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[110.59,122.19]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.21,156.73],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.24,166.65],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.65,166.9],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.32,157.04],"t":360},{"s":[158.21,156.73],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-8.22],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-59.24],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-134.24],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-189.36],"t":360},{"s":[-368.21],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":91.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[60.803845623276075,0],[0,67.20386414551082],[-60.803845623276075,0],[0,-67.20386414551082]],"o":[[0,67.20386414551082],[-60.803845623276075,0],[0,-67.20386414551082],[60.803845623276075,0],[0,0]],"v":[[220.1800028643779,121.68998458713644],[110.09000143218896,243.37001009541314],[0,121.68998458713644],[110.09000143218896,0],[220.1800028643779,121.68998458713644]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":3},{"ty":4,"nm":"Ellipse 3_3","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[106.46,123.73]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.3,156.69],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.27,166.55],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.56,166.84],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.23,157.08],"t":360},{"s":[158.3,156.69],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-12.34],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-63.36],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-138.36],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-193.48],"t":360},{"s":[-372.33],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":87.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[58.5238432623317,0],[0,68.0638418763556],[-58.5238432623317,0],[0,-68.0638418763556]],"o":[[0,68.0638418763556],[-58.5238432623317,0],[0,-68.0638418763556],[58.5238432623317,0],[0,0]],"v":[[211.92001203087867,123.23001397953581],[105.96000601543933,246.46998754963005],[0,123.23001397953581],[105.96000601543933,0],[211.92001203087867,123.23001397953581]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":4},{"ty":4,"nm":"Ellipse 3_4","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[102.33,125.28]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.4,156.65],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.29,166.44],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.47,166.79],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.13,157.13],"t":360},{"s":[158.4,156.65],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-16.46],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-67.48],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-142.48],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-197.6],"t":360},{"s":[-376.45],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":83.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[56.2438445815577,0],[0,68.91385477034008],[-56.2438445815577,0],[0,-68.91385477034008]],"o":[[0,68.91385477034008],[-56.2438445815577,0],[0,-68.91385477034008],[56.2438445815577,0],[0,0]],"v":[[203.66998709319557,124.7799931439255],[101.83001797648265,249.559986287851],[0,124.7799931439255],[101.83001797648265,0],[203.66998709319557,124.7799931439255]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":5},{"ty":4,"nm":"Ellipse 3_5","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[98.21,126.82]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.5,156.58],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.31,166.33],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.36,166.75],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.04,157.2],"t":360},{"s":[158.5,156.58],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-20.59],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-71.61],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-146.61],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-201.73],"t":360},{"s":[-380.58],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":79.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[53.96384903662216,0],[0,69.7638623002079],[-53.96384903662216,0],[0,-69.7638623002079]],"o":[[0,69.7638623002079],[-53.96384903662216,0],[0,-69.7638623002079],[53.96384903662216,0],[0,0]],"v":[[195.41002192630822,126.32000309697837],[97.70998550675999,252.64000619395674],[0,126.32000309697837],[97.70998550675999,0],[195.41002192630822,126.32000309697837]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":6},{"ty":4,"nm":"Ellipse 3_6","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[94.08,128.36]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.59,156.51],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.3,166.21],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.25,166.72],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.95,157.27],"t":360},{"s":[158.59,156.51],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-24.71],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-75.73],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-150.73],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-205.85],"t":360},{"s":[-384.7],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":75}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[51.68385605468561,0],[0,70.6138650044132],[-51.68385605468561,0],[0,-70.6138650044132]],"o":[[0,70.6138650044132],[-51.68385605468561,0],[0,-70.6138650044132],[51.68385605468561,0],[0,0]],"v":[[187.16001453521255,127.86000440071207],[93.58000726760628,255.72000880142414],[0,127.86000440071207],[93.58000726760628,0],[187.16001453521255,127.86000440071207]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":7},{"ty":4,"nm":"Ellipse 3_7","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[89.96,129.9]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.67,156.42],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.29,166.1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.13,166.7],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.87,157.36],"t":360},{"s":[158.67,156.42],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-28.84],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-79.86],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-154.86],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-209.98],"t":360},{"s":[-388.83],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":70.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[49.40386474698453,0],[0,71.46386339534449],[-49.40386474698453,0],[0,-71.46386339534449]],"o":[[0,71.46386339534449],[-49.40386474698453,0],[0,-71.46386339534449],[49.40386474698453,0],[0,0]],"v":[[178.9100101978661,129.3999979795825],[89.45997730754284,258.799995959165],[0,129.3999979795825],[89.45997730754284,0],[178.9100101978661,129.3999979795825]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":8},{"ty":4,"nm":"Ellipse 3_8","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[85.83,131.44]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.75,156.33],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.27,165.98],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.01,166.69],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.79,157.45],"t":360},{"s":[158.75,156.33],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-32.97],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-83.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-158.99],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-214.11],"t":360},{"s":[-392.96],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":66.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[47.12387425033133,0],[0,72.31385820142478],[-47.12387425033133,0],[0,-72.31385820142478]],"o":[[0,72.31385820142478],[-47.12387425033133,0],[0,-72.31385820142478],[47.12387425033133,0],[0,0]],"v":[[170.66000535468095,130.9399851494045],[85.33000267734047,261.8700083401093],[0,130.9399851494045],[85.33000267734047,0],[170.66000535468095,130.9399851494045]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":9},{"ty":4,"nm":"Ellipse 3_9","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[81.7,132.97]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.81,156.24],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.23,165.87],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.9,166.7],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.73,157.55],"t":360},{"s":[158.81,156.24],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-37.1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-88.12],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-163.12],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-218.24],"t":360},{"s":[-397.09],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":62.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[44.843883630479816,0],[0,73.16385002206691],[-44.843883630479816,0],[0,-73.16385002206691]],"o":[[0,73.16385002206691],[-44.843883630479816,0],[0,-73.16385002206691],[44.843883630479816,0],[0,0]],"v":[[162.4099961005476,132.4700045946928],[81.20002864827575,264.9400091893856],[0,132.4700045946928],[81.20002864827575,0],[162.4099961005476,132.4700045946928]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":10},{"ty":4,"nm":"Ellipse 3_10","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[77.58,134.51]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.86,156.14],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.19,165.77],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.79,166.72],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.68,157.65],"t":360},{"s":[158.86,156.14],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-41.23],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-92.25],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-167.25],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-222.37],"t":360},{"s":[-401.22],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":58.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[42.573827167754324,0],[0,74.01383964845107],[-42.573827167754324,0],[0,-74.00387682173839]],"o":[[0,74.01383964845107],[-42.573827167754324,0],[0,-74.00387682173839],[42.573827167754324,0],[0,0]],"v":[[154.15997720767237,134.00998211217058],[77.07998860383618,268.01000139762846],[0,134.00998211217058],[77.07998860383618,0],[154.15997720767237,134.00998211217058]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":11},{"ty":4,"nm":"Ellipse 3_11","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[73.45,136.04]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.9,156.04],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.14,165.68],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.69,166.74],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.64,157.75],"t":360},{"s":[158.9,156.04],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-45.36],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-96.38],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-171.38],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-226.5],"t":360},{"s":[-405.35],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":54.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[40.293829019253785,0],[0,74.85386471427232],[-40.293829019253785,0],[0,-74.85386471427232]],"o":[[0,74.85386471427232],[-40.293829019253785,0],[0,-74.85386471427232],[40.293829019253785,0],[0,0]],"v":[[145.90001190358348,135.53999408664455],[72.95000595179174,271.0799881732891],[0,135.53999408664455],[72.95000595179174,0],[145.90001190358348,135.53999408664455]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":12},{"ty":4,"nm":"Ellipse 3_12","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[69.32,137.57]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.93,155.95],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[169.08,165.6],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.59,166.77],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.62,157.84],"t":360},{"s":[158.93,155.95],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-49.49],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-100.51],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-175.51],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-230.63],"t":360},{"s":[-409.48],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":50}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[38.013826532083414,0],[0,75.70385201906329],[-38.013826532083414,0],[0,-75.70385201906329]],"o":[[0,75.70385201906329],[-38.013826532083414,0],[0,-75.70385201906329],[38.013826532083414,0],[0,0]],"v":[[137.6499620964577,137.07000424141268],[68.8200171116224,274.14000848282535],[0,137.07000424141268],[68.8200171116224,0],[137.6499620964577,137.07000424141268]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":13},{"ty":4,"nm":"Ellipse 3_13","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[65.19,139.1]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.94,155.86],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.33,166.76],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.52,166.82],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.55,157],"t":360},{"s":[158.94,155.86],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-53.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[74.45],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[179.45],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[29.45],"t":360},{"s":[-53.61],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":45.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[35.72389476970868,0],[0,76.54387542681734],[-35.72389476970868,0],[0,-76.55383948245738]],"o":[[0,76.54387542681734],[-35.72389476970868,0],[0,-76.55383948245738],[35.72389476970868,0],[0,0]],"v":[[129.38996219506427,138.60001395301083],[64.6900194454808,277.2099919616617],[0,138.60001395301083],[64.6900194454808,0],[129.38996219506427,138.60001395301083]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":14},{"ty":4,"nm":"Ellipse 3_14","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[61.06,140.64]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.95,155.79],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.38,166.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.45,166.86],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.63,157],"t":360},{"s":[158.95,155.79],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-57.74],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[70.32],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[175.32],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[25.32],"t":360},{"s":[-57.74],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":41.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[33.44388373067606,0],[0,77.39386355069978],[-33.44388373067606,0],[0,-77.39386355069978]],"o":[[0,77.39386355069978],[-33.44388373067606,0],[0,-77.39386355069978],[33.44388373067606,0],[0,0]],"v":[[121.12002020136896,140.13998931080747],[60.56001010068448,280.27001417413834],[0,140.13998931080747],[60.56001010068448,0],[121.12002020136896,140.13998931080747]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":15},{"ty":4,"nm":"Ellipse 3_15","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[56.93,142.17]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.95,155.72],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.44,166.85],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.4,166.9],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.69,156.98],"t":360},{"s":[158.95,155.72],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-61.86],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[66.2],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[171.2],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[21.2],"t":360},{"s":[-61.86],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":37.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[31.16386355874014,0],[0,78.24385347190263],[-31.16386355874014,0],[0,-78.24385347190263]],"o":[[0,78.24385347190263],[-31.16386355874014,0],[0,-78.24385347190263],[31.16386355874014,0],[0,0]],"v":[[112.85997152665878,141.67000315970958],[56.42998576332939,283.34000631941916],[0,141.67000315970958],[56.42998576332939,0],[112.85997152665878,141.67000315970958]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":16},{"ty":4,"nm":"Ellipse 3_16","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[52.79,143.71]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.94,155.67],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.48,166.87],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.37,166.94],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.74,156.97],"t":360},{"s":[158.94,155.67],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-65.98],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[62.08],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.08],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[17.08],"t":360},{"s":[-65.98],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":33.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[28.883832027456823,0],[0,79.09384580745224],[-28.883832027456823,0],[0,-79.09384580745224]],"o":[[0,79.09384580745224],[-28.883832027456823,0],[0,-79.09384580745224],[28.883832027456823,0],[0,0]],"v":[[104.5899802393404,143.20998590133505],[52.290037473574955,286.4100065959412],[0,143.20998590133505],[52.290037473574955,0],[104.5899802393404,143.20998590133505]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":17},{"ty":4,"nm":"Ellipse 3_17","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[48.66,145.24]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.93,155.63],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.52,166.89],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.34,166.98],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.78,156.95],"t":360},{"s":[158.93,155.63],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-70.09],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[57.97],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[162.97],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[12.97],"t":360},{"s":[-70.09],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":29.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[26.593889126174012,0],[0,79.94384132234322],[-26.593889126174012,0],[0,-79.94384132234322]],"o":[[0,79.94384132234322],[-26.593889126174012,0],[0,-79.94384132234322],[26.593889126174012,0],[0,0]],"v":[[96.3199586810292,144.74000889624944],[48.1599793405146,289.48998336733337],[0,144.74000889624944],[48.1599793405146,0],[96.3199586810292,144.74000889624944]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":18},{"ty":4,"nm":"Ellipse 3_18","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[44.52,146.78]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.91,155.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.55,166.89],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.34,167],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.8,156.93],"t":360},{"s":[158.91,155.61],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-74.2],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[53.86],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.86],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[8.86],"t":360},{"s":[-74.2],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":25}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[24.31383537229772,0],[0,80.79384043362168],[-24.31383537229772,0],[0,-80.783874497957]],"o":[[0,80.79384043362168],[-24.31383537229772,0],[0,-80.783874497957],[24.31383537229772,0],[0,0]],"v":[[88.04001226309025,146.28000368628335],[44.020006131545124,292.5600073725667],[0,146.28000368628335],[44.020006131545124,0],[88.04001226309025,146.28000368628335]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":19},{"ty":4,"nm":"Ellipse 3_19","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[40.38,148.32]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.89,155.6],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.56,166.88],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.34,167.02],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.8,156.92],"t":360},{"s":[158.89,155.6],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-78.31],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[49.75],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[154.75],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[4.75],"t":360},{"s":[-78.31],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":20.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[22.023885158588843,0],[0,81.64384379647754],[-22.023885158588843,0],[0,-81.64384379647754]],"o":[[0,81.64384379647754],[-22.023885158588843,0],[0,-81.64384379647754],[22.023885158588843,0],[0,0]],"v":[[79.7600432918222,147.82000624870054],[39.8800216459111,295.6400124974011],[0,147.82000624870054],[39.8800216459111,0],[79.7600432918222,147.82000624870054]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":20},{"ty":4,"nm":"Ellipse 3_20","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[36.24,149.87]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.88,155.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.57,166.86],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.36,167.03],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.79,156.9],"t":360},{"s":[158.88,155.61],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-82.41],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[45.65],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.65],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[0.65],"t":360},{"s":[-82.41],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":16.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[19.743799200404776,0],[0,82.49385167325761],[-19.743799200404776,0],[0,-82.49385167325761]],"o":[[0,82.49385167325761],[-19.743799200404776,0],[0,-82.49385167325761],[19.743799200404776,0],[0,0]],"v":[[71.4800482540129,149.36998369301824],[35.74002412700645,298.7300007493718],[0,149.36998369301824],[35.74002412700645,0],[71.4800482540129,149.36998369301824]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":21},{"ty":4,"nm":"Ellipse 3_21","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[32.1,151.41]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.64],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.55,166.84],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.38,167.02],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.77,156.9],"t":360},{"s":[158.87,155.64],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-86.51],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[41.55],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[146.55],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-3.45],"t":360},{"s":[-86.51],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":12.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[17.45383822781624,0],[0,83.34386437080049],[-17.45383822781624,0],[0,-83.34386437080049]],"o":[[0,83.34386437080049],[-17.45383822781624,0],[0,-83.34386437080049],[17.45383822781624,0],[0,0]],"v":[[63.20002336298944,150.91000364007297],[31.60001168149472,301.82000728014594],[0,150.91000364007297],[31.60001168149472,0],[63.20002336298944,150.91000364007297]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":22},{"ty":4,"nm":"Ellipse 3_22","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[27.96,152.96]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.68],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.52,166.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.42,167],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.73,156.9],"t":360},{"s":[158.87,155.68],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-90.6],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[37.46],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[142.46],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-7.54],"t":360},{"s":[-90.6],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":8.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[15.163881073920606,0],[0,84.20384943960175],[-15.163881073920606,0],[0,-84.20384943960175]],"o":[[0,84.20384943960175],[-15.163881073920606,0],[0,-84.20384943960175],[15.163881073920606,0],[0,0]],"v":[[54.91996306046894,152.4600001217032],[27.45998153023447,304.9200002434064],[0,152.4600001217032],[27.45998153023447,0],[54.91996306046894,152.4600001217032]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":23},{"ty":4,"nm":"Ellipse 3_23","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[23.82,154.51]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.73],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.48,166.78],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.46,166.96],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.67,156.91],"t":360},{"s":[158.87,155.73],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-94.69],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[33.37],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[138.37],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-11.63],"t":360},{"s":[-94.69],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":4.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[12.873932520948756,0],[0,85.06384015320415],[-12.873932520948756,0],[0,-85.05387251315808]],"o":[[0,85.06384015320415],[-12.873932520948756,0],[0,-85.05387251315808],[12.873932520948756,0],[0,0]],"v":[[46.63006889526915,154.01000635190502],[23.31992947901136,308.02001270381004],[0,154.01000635190502],[23.31992947901136,0],[46.63006889526915,154.01000635190502]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":24},{"ty":4,"nm":"Ellipse 3_24","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[19.67,156.07]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.89,155.79],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.42,166.75],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.5,166.91],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.61,156.93],"t":360},{"s":[158.89,155.79],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-98.78],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[29.28],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[134.28],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-15.72],"t":360},{"s":[-98.78],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":0}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[10.593747315399902,0],[0,85.91386867826684],[-10.593747315399902,0],[0,-85.91386867826684]],"o":[[0,85.91386867826684],[-10.593747315399902,0],[0,-85.91386867826684],[10.593747315399902,0],[0,0]],"v":[[38.3499500332094,155.5699905397042],[19.17010208775677,311.1300131169211],[0,155.5699905397042],[19.17010208775677,0],[38.3499500332094,155.5699905397042]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":25}]},{"nm":"[Asset] Mask group","id":"2","layers":[{"ty":0,"nm":"LooperGroup","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"td":1,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.25,157.26],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[149.72,157.42],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[165.48,166.58],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.85,153.7],"t":360},{"s":[157.25,157.26],"t":480}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.15,156.14],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.36,148.6],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[165.41,164.3],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.58,157.76],"t":360},{"s":[156.15,156.14],"t":480}]},"r":{"a":0,"k":90.41},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":316.81,"h":319.05,"refId":"3","ind":1},{"ty":4,"nm":"Rectangle 44","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"tt":1,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[156.88,163.81]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.4,150.7],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.61,143.16],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[170.66,158.93],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.83,152.29],"t":360},{"s":[161.4,150.7],"t":480}]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[313.76,0],[313.76,327.62],[0,327.62],[0,0]]}}},{"ty":"gf","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[156.8820037841797,327.6239929199219],"t":360},{"s":[156.8820037841797,327.6239929199219],"t":480}]},"g":{"p":3,"k":{"a":0,"k":[0,0.6787450980392158,0.5298431372549021,1,0.7596153616905212,0.5048627450980393,0.6451372549019607,1,1,0.3122745098039216,0.5873725490196079,1]}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[157.11399841308594,33.78219985961914],"t":360},{"s":[157.11399841308594,33.78219985961914],"t":480}]},"r":1,"o":{"a":0,"k":100}}],"ind":2}]},{"nm":"[Asset] LooperGroup","id":"3","layers":[{"ty":4,"nm":"Ellipse 3","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[118.85,119.08]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.03,156.76],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.49,156.93],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.66,166.76],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.57,154.03],"t":360},{"s":[158.03,156.76],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[0],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-0.63],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-125.45],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-98.84],"t":360},{"s":[0],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[65.36386305921113,0],[0,65.4938515227654],[-65.36386305921113,0],[0,-65.4938515227654]],"o":[[0,65.4938515227654],[-65.36386305921113,0],[0,-65.4938515227654],[65.36386305921113,0],[0,0]],"v":[[236.70999219690856,118.58001884340739],[118.35001713265146,237.1699956998181],[0,118.58001884340739],[118.35001713265146,0],[236.70999219690856,118.58001884340739]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":1},{"ty":4,"nm":"Ellipse 3_1","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[114.72,120.64]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.11,156.75],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.58,156.92],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.6,166.7],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.55,153.95],"t":360},{"s":[158.11,156.75],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-4.11],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-4.74],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-129.56],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-102.95],"t":360},{"s":[-4.11],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":95.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[63.08385213008413,0],[0,66.35383995118667],[-63.08385213008413,0],[0,-66.35383995118667]],"o":[[0,66.35383995118667],[-63.08385213008413,0],[0,-66.35383995118667],[63.08385213008413,0],[0,0]],"v":[[228.44000998621374,120.13998576783972],[114.22000499310687,240.2700129824825],[0,120.13998576783972],[114.22000499310687,0],[228.44000998621374,120.13998576783972]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":2},{"ty":4,"nm":"Ellipse 3_2","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[110.59,122.19]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.21,156.73],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.67,156.9],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.53,166.63],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.51,153.86],"t":360},{"s":[158.21,156.73],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-8.22],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-8.85],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-133.67],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-107.06],"t":360},{"s":[-8.22],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":91.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[60.803845623276075,0],[0,67.20386414551082],[-60.803845623276075,0],[0,-67.20386414551082]],"o":[[0,67.20386414551082],[-60.803845623276075,0],[0,-67.20386414551082],[60.803845623276075,0],[0,0]],"v":[[220.1800028643779,121.68998458713644],[110.09000143218896,243.37001009541314],[0,121.68998458713644],[110.09000143218896,0],[220.1800028643779,121.68998458713644]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":3},{"ty":4,"nm":"Ellipse 3_3","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[106.46,123.73]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.3,156.69],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.77,156.86],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.44,166.57],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.46,153.77],"t":360},{"s":[158.3,156.69],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-12.34],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-12.97],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-137.79],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-111.18],"t":360},{"s":[-12.34],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":87.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[58.5238432623317,0],[0,68.0638418763556],[-58.5238432623317,0],[0,-68.0638418763556]],"o":[[0,68.0638418763556],[-58.5238432623317,0],[0,-68.0638418763556],[58.5238432623317,0],[0,0]],"v":[[211.92001203087867,123.23001397953581],[105.96000601543933,246.46998754963005],[0,123.23001397953581],[105.96000601543933,0],[211.92001203087867,123.23001397953581]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":4},{"ty":4,"nm":"Ellipse 3_4","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[102.33,125.28]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.4,156.65],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.86,156.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.35,166.52],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.4,153.68],"t":360},{"s":[158.4,156.65],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-16.46],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-17.09],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-141.91],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-115.3],"t":360},{"s":[-16.46],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":83.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[56.2438445815577,0],[0,68.91385477034008],[-56.2438445815577,0],[0,-68.91385477034008]],"o":[[0,68.91385477034008],[-56.2438445815577,0],[0,-68.91385477034008],[56.2438445815577,0],[0,0]],"v":[[203.66998709319557,124.7799931439255],[101.83001797648265,249.559986287851],[0,124.7799931439255],[101.83001797648265,0],[203.66998709319557,124.7799931439255]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":5},{"ty":4,"nm":"Ellipse 3_5","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[98.21,126.82]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.5,156.58],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[150.96,156.75],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.24,166.48],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.32,153.6],"t":360},{"s":[158.5,156.58],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-20.59],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-21.22],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-146.04],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-119.43],"t":360},{"s":[-20.59],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":79.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[53.96384903662216,0],[0,69.7638623002079],[-53.96384903662216,0],[0,-69.7638623002079]],"o":[[0,69.7638623002079],[-53.96384903662216,0],[0,-69.7638623002079],[53.96384903662216,0],[0,0]],"v":[[195.41002192630822,126.32000309697837],[97.70998550675999,252.64000619395674],[0,126.32000309697837],[97.70998550675999,0],[195.41002192630822,126.32000309697837]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":6},{"ty":4,"nm":"Ellipse 3_6","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[94.08,128.36]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.59,156.51],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.05,156.67],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.13,166.45],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.24,153.52],"t":360},{"s":[158.59,156.51],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-24.71],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-25.34],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-150.16],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-123.55],"t":360},{"s":[-24.71],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":75}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[51.68385605468561,0],[0,70.6138650044132],[-51.68385605468561,0],[0,-70.6138650044132]],"o":[[0,70.6138650044132],[-51.68385605468561,0],[0,-70.6138650044132],[51.68385605468561,0],[0,0]],"v":[[187.16001453521255,127.86000440071207],[93.58000726760628,255.72000880142414],[0,127.86000440071207],[93.58000726760628,0],[187.16001453521255,127.86000440071207]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":7},{"ty":4,"nm":"Ellipse 3_7","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[89.96,129.9]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.67,156.42],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.13,156.59],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.01,166.43],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.14,153.45],"t":360},{"s":[158.67,156.42],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-28.84],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-29.47],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-154.29],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-127.68],"t":360},{"s":[-28.84],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":70.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[49.40386474698453,0],[0,71.46386339534449],[-49.40386474698453,0],[0,-71.46386339534449]],"o":[[0,71.46386339534449],[-49.40386474698453,0],[0,-71.46386339534449],[49.40386474698453,0],[0,0]],"v":[[178.9100101978661,129.3999979795825],[89.45997730754284,258.799995959165],[0,129.3999979795825],[89.45997730754284,0],[178.9100101978661,129.3999979795825]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":8},{"ty":4,"nm":"Ellipse 3_8","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[85.83,131.44]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.75,156.33],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.2,156.5],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.89,166.42],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[160.04,153.39],"t":360},{"s":[158.75,156.33],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-32.97],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-33.6],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-158.42],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-131.81],"t":360},{"s":[-32.97],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":66.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[47.12387425033133,0],[0,72.31385820142478],[-47.12387425033133,0],[0,-72.31385820142478]],"o":[[0,72.31385820142478],[-47.12387425033133,0],[0,-72.31385820142478],[47.12387425033133,0],[0,0]],"v":[[170.66000535468095,130.9399851494045],[85.33000267734047,261.8700083401093],[0,130.9399851494045],[85.33000267734047,0],[170.66000535468095,130.9399851494045]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":9},{"ty":4,"nm":"Ellipse 3_9","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[81.7,132.97]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.81,156.24],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.27,156.4],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.78,166.43],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.93,153.34],"t":360},{"s":[158.81,156.24],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-37.1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-37.73],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-162.55],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-135.94],"t":360},{"s":[-37.1],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":62.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[44.843883630479816,0],[0,73.16385002206691],[-44.843883630479816,0],[0,-73.16385002206691]],"o":[[0,73.16385002206691],[-44.843883630479816,0],[0,-73.16385002206691],[44.843883630479816,0],[0,0]],"v":[[162.4099961005476,132.4700045946928],[81.20002864827575,264.9400091893856],[0,132.4700045946928],[81.20002864827575,0],[162.4099961005476,132.4700045946928]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":10},{"ty":4,"nm":"Ellipse 3_10","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[77.58,134.51]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.86,156.14],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.32,156.3],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.67,166.44],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.83,153.31],"t":360},{"s":[158.86,156.14],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-41.23],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-41.86],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-166.68],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-140.07],"t":360},{"s":[-41.23],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":58.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[42.573827167754324,0],[0,74.01383964845107],[-42.573827167754324,0],[0,-74.00387682173839]],"o":[[0,74.01383964845107],[-42.573827167754324,0],[0,-74.00387682173839],[42.573827167754324,0],[0,0]],"v":[[154.15997720767237,134.00998211217058],[77.07998860383618,268.01000139762846],[0,134.00998211217058],[77.07998860383618,0],[154.15997720767237,134.00998211217058]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":11},{"ty":4,"nm":"Ellipse 3_11","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[73.45,136.04]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.9,156.04],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.36,156.2],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.57,166.47],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.73,153.28],"t":360},{"s":[158.9,156.04],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-45.36],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-45.99],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-170.81],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-144.2],"t":360},{"s":[-45.36],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":54.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[40.293829019253785,0],[0,74.85386471427232],[-40.293829019253785,0],[0,-74.85386471427232]],"o":[[0,74.85386471427232],[-40.293829019253785,0],[0,-74.85386471427232],[40.293829019253785,0],[0,0]],"v":[[145.90001190358348,135.53999408664455],[72.95000595179174,271.0799881732891],[0,135.53999408664455],[72.95000595179174,0],[145.90001190358348,135.53999408664455]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":12},{"ty":4,"nm":"Ellipse 3_12","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[69.32,137.57]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.93,155.95],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.38,156.11],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.48,166.5],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.63,153.27],"t":360},{"s":[158.93,155.95],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-49.49],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-50.12],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-174.94],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-148.33],"t":360},{"s":[-49.49],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":50}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[38.013826532083414,0],[0,75.70385201906329],[-38.013826532083414,0],[0,-75.70385201906329]],"o":[[0,75.70385201906329],[-38.013826532083414,0],[0,-75.70385201906329],[38.013826532083414,0],[0,0]],"v":[[137.6499620964577,137.07000424141268],[68.8200171116224,274.14000848282535],[0,137.07000424141268],[68.8200171116224,0],[137.6499620964577,137.07000424141268]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":13},{"ty":4,"nm":"Ellipse 3_13","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[65.19,139.1]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.94,155.86],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.22,156.82],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.4,166.54],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.04,152.54],"t":360},{"s":[158.94,155.86],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-53.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[25.3],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[180.23],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[343.64],"t":360},{"s":[306.38],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":45.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[35.72389476970868,0],[0,76.54387542681734],[-35.72389476970868,0],[0,-76.55383948245738]],"o":[[0,76.54387542681734],[-35.72389476970868,0],[0,-76.55383948245738],[35.72389476970868,0],[0,0]],"v":[[129.38996219506427,138.60001395301083],[64.6900194454808,277.2099919616617],[0,138.60001395301083],[64.6900194454808,0],[129.38996219506427,138.60001395301083]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":14},{"ty":4,"nm":"Ellipse 3_14","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[61.06,140.64]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.95,155.79],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.3,156.81],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.33,166.58],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.09,152.48],"t":360},{"s":[158.95,155.79],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-57.74],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[21.17],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[176.1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[339.51],"t":360},{"s":[302.25],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":41.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[33.44388373067606,0],[0,77.39386355069978],[-33.44388373067606,0],[0,-77.39386355069978]],"o":[[0,77.39386355069978],[-33.44388373067606,0],[0,-77.39386355069978],[33.44388373067606,0],[0,0]],"v":[[121.12002020136896,140.13998931080747],[60.56001010068448,280.27001417413834],[0,140.13998931080747],[60.56001010068448,0],[121.12002020136896,140.13998931080747]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":15},{"ty":4,"nm":"Ellipse 3_15","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[56.93,142.17]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.95,155.72],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.36,156.79],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.28,166.63],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.13,152.42],"t":360},{"s":[158.95,155.72],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-61.86],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[17.05],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[171.98],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[335.39],"t":360},{"s":[298.13],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":37.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[31.16386355874014,0],[0,78.24385347190263],[-31.16386355874014,0],[0,-78.24385347190263]],"o":[[0,78.24385347190263],[-31.16386355874014,0],[0,-78.24385347190263],[31.16386355874014,0],[0,0]],"v":[[112.85997152665878,141.67000315970958],[56.42998576332939,283.34000631941916],[0,141.67000315970958],[56.42998576332939,0],[112.85997152665878,141.67000315970958]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":16},{"ty":4,"nm":"Ellipse 3_16","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[52.79,143.71]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.94,155.67],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.41,156.78],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.25,166.66],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.15,152.38],"t":360},{"s":[158.94,155.67],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-65.98],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[12.93],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.86],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[331.27],"t":360},{"s":[294.01],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":33.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[28.883832027456823,0],[0,79.09384580745224],[-28.883832027456823,0],[0,-79.09384580745224]],"o":[[0,79.09384580745224],[-28.883832027456823,0],[0,-79.09384580745224],[28.883832027456823,0],[0,0]],"v":[[104.5899802393404,143.20998590133505],[52.290037473574955,286.4100065959412],[0,143.20998590133505],[52.290037473574955,0],[104.5899802393404,143.20998590133505]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":17},{"ty":4,"nm":"Ellipse 3_17","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[48.66,145.24]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.93,155.63],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.45,156.75],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.22,166.7],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.16,152.34],"t":360},{"s":[158.93,155.63],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-70.09],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[8.82],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[163.75],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[327.16],"t":360},{"s":[289.9],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":29.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[26.593889126174012,0],[0,79.94384132234322],[-26.593889126174012,0],[0,-79.94384132234322]],"o":[[0,79.94384132234322],[-26.593889126174012,0],[0,-79.94384132234322],[26.593889126174012,0],[0,0]],"v":[[96.3199586810292,144.74000889624944],[48.1599793405146,289.48998336733337],[0,144.74000889624944],[48.1599793405146,0],[96.3199586810292,144.74000889624944]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":18},{"ty":4,"nm":"Ellipse 3_18","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[44.52,146.78]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.91,155.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.47,156.73],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.22,166.72],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.16,152.31],"t":360},{"s":[158.91,155.61],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-74.2],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[4.71],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[159.64],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[323.05],"t":360},{"s":[285.79],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":25}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[24.31383537229772,0],[0,80.79384043362168],[-24.31383537229772,0],[0,-80.783874497957]],"o":[[0,80.79384043362168],[-24.31383537229772,0],[0,-80.783874497957],[24.31383537229772,0],[0,0]],"v":[[88.04001226309025,146.28000368628335],[44.020006131545124,292.5600073725667],[0,146.28000368628335],[44.020006131545124,0],[88.04001226309025,146.28000368628335]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":19},{"ty":4,"nm":"Ellipse 3_19","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[40.38,148.32]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.89,155.6],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.47,156.72],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.22,166.74],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.15,152.3],"t":360},{"s":[158.89,155.6],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-78.31],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[0.6],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[155.53],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[318.94],"t":360},{"s":[281.68],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":20.83}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[22.023885158588843,0],[0,81.64384379647754],[-22.023885158588843,0],[0,-81.64384379647754]],"o":[[0,81.64384379647754],[-22.023885158588843,0],[0,-81.64384379647754],[22.023885158588843,0],[0,0]],"v":[[79.7600432918222,147.82000624870054],[39.8800216459111,295.6400124974011],[0,147.82000624870054],[39.8800216459111,0],[79.7600432918222,147.82000624870054]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":20},{"ty":4,"nm":"Ellipse 3_20","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[36.24,149.87]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.88,155.61],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.46,156.71],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.24,166.75],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.14,152.3],"t":360},{"s":[158.88,155.61],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-82.41],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-3.5],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[151.43],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[314.84],"t":360},{"s":[277.58],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":16.67}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[19.743799200404776,0],[0,82.49385167325761],[-19.743799200404776,0],[0,-82.49385167325761]],"o":[[0,82.49385167325761],[-19.743799200404776,0],[0,-82.49385167325761],[19.743799200404776,0],[0,0]],"v":[[71.4800482540129,149.36998369301824],[35.74002412700645,298.7300007493718],[0,149.36998369301824],[35.74002412700645,0],[71.4800482540129,149.36998369301824]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":21},{"ty":4,"nm":"Ellipse 3_21","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[32.1,151.41]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.64],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.43,156.7],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.26,166.74],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.11,152.31],"t":360},{"s":[158.87,155.64],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-86.51],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-7.6],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[147.33],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[310.74],"t":360},{"s":[273.48],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":12.5}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[17.45383822781624,0],[0,83.34386437080049],[-17.45383822781624,0],[0,-83.34386437080049]],"o":[[0,83.34386437080049],[-17.45383822781624,0],[0,-83.34386437080049],[17.45383822781624,0],[0,0]],"v":[[63.20002336298944,150.91000364007297],[31.60001168149472,301.82000728014594],[0,150.91000364007297],[31.60001168149472,0],[63.20002336298944,150.91000364007297]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":22},{"ty":4,"nm":"Ellipse 3_22","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[27.96,152.96]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.68],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.39,156.71],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.3,166.72],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.09,152.34],"t":360},{"s":[158.87,155.68],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-90.6],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-11.69],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[143.24],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[306.65],"t":360},{"s":[269.39],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":8.33}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[15.163881073920606,0],[0,84.20384943960175],[-15.163881073920606,0],[0,-84.20384943960175]],"o":[[0,84.20384943960175],[-15.163881073920606,0],[0,-84.20384943960175],[15.163881073920606,0],[0,0]],"v":[[54.91996306046894,152.4600001217032],[27.45998153023447,304.9200002434064],[0,152.4600001217032],[27.45998153023447,0],[54.91996306046894,152.4600001217032]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":23},{"ty":4,"nm":"Ellipse 3_23","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[23.82,154.51]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.87,155.73],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.34,156.72],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.34,166.68],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.06,152.39],"t":360},{"s":[158.87,155.73],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-94.69],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-15.78],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[139.15],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[302.56],"t":360},{"s":[265.3],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":4.17}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[12.873932520948756,0],[0,85.06384015320415],[-12.873932520948756,0],[0,-85.05387251315808]],"o":[[0,85.06384015320415],[-12.873932520948756,0],[0,-85.05387251315808],[12.873932520948756,0],[0,0]],"v":[[46.63006889526915,154.01000635190502],[23.31992947901136,308.02001270381004],[0,154.01000635190502],[23.31992947901136,0],[46.63006889526915,154.01000635190502]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":24},{"ty":4,"nm":"Ellipse 3_24","sr":1,"st":0,"op":481.24,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[19.67,156.07]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[158.89,155.79],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[152.28,156.75],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[166.38,166.63],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[161.03,152.45],"t":360},{"s":[158.89,155.79],"t":480}]},"r":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-98.78],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[-19.87],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[135.06],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[298.47],"t":360},{"s":[261.21],"t":480}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":0}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[10.593747315399902,0],[0,85.91386867826684],[-10.593747315399902,0],[0,-85.91386867826684]],"o":[[0,85.91386867826684],[-10.593747315399902,0],[0,-85.91386867826684],[10.593747315399902,0],[0,0]],"v":[[38.3499500332094,155.5699905397042],[19.17010208775677,311.1300131169211],[0,155.5699905397042],[19.17010208775677,0],[38.3499500332094,155.5699905397042]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":120},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":240},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[1],"t":360},{"s":[1],"t":480}]},"c":{"a":0,"k":[0,0,0]}}],"ind":25}]}]} \ No newline at end of file diff --git a/assets/lotties/ai_loader.json b/assets/lotties/ai_loader.json new file mode 100644 index 0000000..89c683b --- /dev/null +++ b/assets/lotties/ai_loader.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":512,"w":512,"meta":{"g":"@lottiefiles/creator 1.53.2"},"layers":[{"ty":0,"nm":"all stars","sr":1,"st":-89,"op":65,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256,256,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":512,"h":512,"refId":"comp_0_d71ae482-3da5-4ff1-aa2f-1d14bd1b8c1d","ind":1},{"ty":0,"nm":"all stars","sr":1,"st":92,"op":181,"ip":92,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256,256,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":512,"h":512,"refId":"comp_0_d71ae482-3da5-4ff1-aa2f-1d14bd1b8c1d","ind":2},{"ty":0,"nm":"all stars","sr":1,"st":0,"op":138,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256,256,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"w":512,"h":512,"refId":"comp_0_d71ae482-3da5-4ff1-aa2f-1d14bd1b8c1d","ind":3}],"v":"5.7.0","fr":60,"op":181,"ip":0,"assets":[{"nm":"Scene","id":"comp_0_d71ae482-3da5-4ff1-aa2f-1d14bd1b8c1d","layers":[{"ty":4,"nm":"Star_3","sr":1,"st":44,"op":154,"ip":44,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[78,90,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.01,"y":0},"i":{"x":0.271,"y":1},"s":[0,0,100],"t":44},{"o":{"x":0.507,"y":0},"i":{"x":0.976,"y":1},"s":[70,70,100],"t":104},{"s":[0,0,100],"t":154}],"ix":6},"sk":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"o":{"x":0.363,"y":0},"i":{"x":0.554,"y":1},"s":[222.23],"t":54},{"s":[347.517],"t":148}],"ix":3},"y":{"a":1,"k":[{"o":{"x":0.355,"y":0},"i":{"x":0.576,"y":1},"s":[176.825],"t":54},{"s":[324.452],"t":148}],"ix":4},"z":{"a":0,"k":0}},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":44},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[70],"t":54},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[70],"t":147},{"s":[1],"t":154}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-2.086,-6.184],[0,0],[-3.246,-1.605],[0,0],[5.062,-2.502],[0,0],[1.157,-3.431],[0,0],[2.086,6.184],[0,0],[3.246,1.604],[0,0],[-5.061,2.501],[0,0],[-1.157,3.431]],"o":[[2.086,-6.184],[0,0],[1.157,3.431],[0,0],[5.062,2.501],[0,0],[-3.246,1.604],[0,0],[-2.086,6.184],[0,0],[-1.157,-3.431],[0,0],[-5.061,-2.502],[0,0],[3.246,-1.605],[0,0]],"v":[[-6.69,-85.362],[6.227,-85.362],[22.991,-35.658],[29.868,-27.793],[73.74,-6.11],[73.74,6.111],[29.868,27.794],[22.991,35.658],[6.227,85.362],[-6.69,85.362],[-23.454,35.658],[-30.331,27.794],[-74.204,6.111],[-74.204,-6.11],[-30.331,-27.793],[-23.454,-35.658]]},"ix":2}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[93.267,235.649],"ix":6},"g":{"p":2,"k":{"a":0,"k":[0,0.09411764705882353,0.5058823529411764,0.9725490196078431,1,0.7137254901960784,0.48627450980392156,0.8156862745098039],"ix":9}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[-222.183,4.04],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[79,90],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Star_2","sr":1,"st":3,"op":113,"ip":3,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[78,90,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.124,"y":0},"i":{"x":0.649,"y":1},"s":[0,0,100],"t":3},{"o":{"x":0.853,"y":0},"i":{"x":0.988,"y":1},"s":[70,70,100],"t":63},{"s":[0,0,100],"t":113}],"ix":6},"sk":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"o":{"x":0.579,"y":0},"i":{"x":0.782,"y":1},"s":[147.73],"t":13},{"s":[311.455],"t":111}],"ix":3},"y":{"a":1,"k":[{"o":{"x":0.506,"y":0},"i":{"x":0.779,"y":1},"s":[280.325],"t":13},{"s":[378.265],"t":111}],"ix":4},"z":{"a":0,"k":0}},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":3},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[70],"t":13},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[70],"t":106},{"s":[1],"t":113}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-2.086,-6.184],[0,0],[-3.246,-1.605],[0,0],[5.062,-2.502],[0,0],[1.157,-3.431],[0,0],[2.086,6.184],[0,0],[3.246,1.604],[0,0],[-5.061,2.501],[0,0],[-1.157,3.431]],"o":[[2.086,-6.184],[0,0],[1.157,3.431],[0,0],[5.062,2.501],[0,0],[-3.246,1.604],[0,0],[-2.086,6.184],[0,0],[-1.157,-3.431],[0,0],[-5.061,-2.502],[0,0],[3.246,-1.605],[0,0]],"v":[[-6.69,-85.362],[6.227,-85.362],[22.991,-35.658],[29.868,-27.793],[73.74,-6.11],[73.74,6.111],[29.868,27.794],[22.991,35.658],[6.227,85.362],[-6.69,85.362],[-23.454,35.658],[-30.331,27.794],[-74.204,6.111],[-74.204,-6.11],[-30.331,-27.793],[-23.454,-35.658]]},"ix":2}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[-36.486,-38.996],"ix":6},"g":{"p":2,"k":{"a":0,"k":[0,0.09411764705882353,0.5058823529411764,0.9725490196078431,1,0.7137254901960784,0.48627450980392156,0.8156862745098039],"ix":9}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[52.717,60.3],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[79,90],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Star_1","sr":1,"st":0,"op":110,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[78,90,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.01,"y":0},"i":{"x":0.252,"y":1},"s":[0,0,100],"t":0},{"o":{"x":0.401,"y":0},"i":{"x":0.667,"y":1},"s":[100,100,100],"t":60},{"s":[0,0,100],"t":110}],"ix":6},"sk":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"o":{"x":0.297,"y":0},"i":{"x":0.587,"y":1},"s":[196.73],"t":10},{"s":[354.58],"t":104}],"ix":3},"y":{"a":1,"k":[{"o":{"x":0.315,"y":0},"i":{"x":0.559,"y":1},"s":[193.825],"t":10},{"s":[317.64],"t":104}],"ix":4},"z":{"a":0,"k":0}},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":10},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":103},{"s":[1],"t":110}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-2.086,-6.184],[0,0],[-3.246,-1.605],[0,0],[5.062,-2.502],[0,0],[1.157,-3.431],[0,0],[2.086,6.184],[0,0],[3.246,1.604],[0,0],[-5.061,2.501],[0,0],[-1.157,3.431]],"o":[[2.086,-6.184],[0,0],[1.157,3.431],[0,0],[5.062,2.501],[0,0],[-3.246,1.604],[0,0],[-2.086,6.184],[0,0],[-1.157,-3.431],[0,0],[-5.061,-2.502],[0,0],[3.246,-1.605],[0,0]],"v":[[-6.69,-85.362],[6.227,-85.362],[22.991,-35.658],[29.868,-27.793],[73.74,-6.11],[73.74,6.111],[29.868,27.794],[22.991,35.658],[6.227,85.362],[-6.69,85.362],[-23.454,35.658],[-30.331,27.794],[-74.204,6.111],[-74.204,-6.11],[-30.331,-27.793],[-23.454,-35.658]]},"ix":2}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[-29.772,78.763],"ix":6},"g":{"p":2,"k":{"a":0,"k":[0,0.09411764705882353,0.5058823529411764,0.9725490196078431,1,0.7137254901960784,0.48627450980392156,0.8156862745098039],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[87.515,14.752],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[79,90],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 2","e":{"a":0,"k":[100,0],"ix":6},"g":{"p":2,"k":{"a":0,"k":[0,0.09411764705882353,0.5058823529411764,0.9725490196078431,1,0.7137254901960784,0.48627450980392156,0.8156862745098039],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[0,0],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}}],"ind":3}]}]} \ No newline at end of file diff --git a/assets/lotties/voice.json b/assets/lotties/voice.json new file mode 100644 index 0000000..7a545da --- /dev/null +++ b/assets/lotties/voice.json @@ -0,0 +1 @@ +{"v":"4.5.9","fr":25,"ip":0,"op":116,"w":470,"h":470,"ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pill top","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[237,220.75,0]},"a":{"a":0,"k":[3.5,-29.75,0]},"s":{"a":0,"k":[100,-100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26.627,55.588]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":20},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect"},{"ty":"tm","s":{"a":0,"k":16,"ix":1},"e":{"a":0,"k":61,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[3.627,-30.815],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":4,"mn":"ADBE Vector Group"}],"ip":0,"op":1000,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"pill down","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[237,220.75,0]},"a":{"a":0,"k":[3.5,-29.75,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26.627,55.588]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":20},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":58,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[3.627,-30.815],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":4,"mn":"ADBE Vector Group"}],"ip":0,"op":1000,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"base","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[236.802,274,0]},"a":{"a":0,"k":[3.25,23.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[3.99,18.391]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":20},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[3.575,23.858],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[123.795,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":1000,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Half rign","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[236.533,234.25,0]},"a":{"a":0,"k":[3,-16.25,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[44.426,44.426]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"tm","s":{"a":0,"k":25,"ix":1},"e":{"a":0,"k":75,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[3.594,-15.692],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[137.481,137.481],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"01","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":116.574,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[130,130,100],"e":[100,100,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.3059,0.2275,0.5098,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"02","ks":{"o":{"a":0,"k":50},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[162,162,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[162,162,100],"e":[99,99,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[99,99,100],"e":[162,162,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[162,162,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[130,130,100],"e":[162,162,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.97,0.97,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_0p97_0p333_0","0p667_0p97_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[162,162,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[-0.002,-0.002,0.333]},"n":["0p667_1_0p333_-0p002","0p667_1_0p333_-0p002","0p667_0p667_0p333_0p333"],"t":116,"s":[100,100,100],"e":[133,133,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0.167]},"n":["0p667_1_0p167_0","0p667_1_0p167_0","0p667_0p667_0p167_0p167"],"t":116.574,"s":[133,133,100],"e":[162,162,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[162,162,100],"e":[99,99,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2833,0,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"03","ks":{"o":{"a":0,"k":30},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[190,190,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[190,190,100],"e":[151,151,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[151,151,100],"e":[195,195,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[195,195,100],"e":[160,160,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[160,160,100],"e":[195,195,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.947,0.947,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_0p947_0p333_0","0p667_0p947_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[195,195,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[-0.012,-0.012,0.333]},"n":["0p667_1_0p333_-0p012","0p667_1_0p333_-0p012","0p667_0p667_0p333_0p333"],"t":116,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0.167]},"n":["0p667_1_0p167_0","0p667_1_0p167_0","0p667_0p667_0p167_0p167"],"t":116.574,"s":[115,115,100],"e":[195,195,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[195,195,100],"e":[99,99,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2833,0,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"04","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[130,130,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":116.574,"s":[100,100,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[130,130,100],"e":[100,100,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.7408,0.6504,0.9696,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"transparent1","ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":22.562,"s":[10],"e":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":41.365,"s":[5],"e":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":62.047,"s":[20],"e":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":80.85,"s":[5],"e":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":99.651,"s":[20],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":116.574,"s":[0],"e":[20]},{"t":131.6162109375}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[224,224,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[224,224,100],"e":[127,127,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[127,127,100],"e":[224,224,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[224,224,100],"e":[145,145,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[145,145,100],"e":[224,224,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.944,0.944,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_0p944_0p333_0","0p667_0p944_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[224,224,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[-0.016,-0.016,0.333]},"n":["0p667_1_0p333_-0p016","0p667_1_0p333_-0p016","0p667_0p667_0p333_0p333"],"t":116,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0.167]},"n":["0p667_1_0p167_0","0p667_1_0p167_0","0p667_0p667_0p167_0p167"],"t":116.574,"s":[115,115,100],"e":[224,224,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[224,224,100],"e":[99,99,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2833,0,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":10,"ty":4,"nm":"transparent2","ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["0p667_1_0p167_0"],"t":0,"s":[100],"e":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":22.562,"s":[0],"e":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":41.365,"s":[100],"e":[0]},{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.333],"y":[0.333]},"n":["0p667_0p667_0p333_0p333"],"t":62.047,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.333],"y":[0.333]},"n":["0p667_0p667_0p333_0p333"],"t":80.85,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.333],"y":[0.333]},"n":["0p667_0p667_0p333_0p333"],"t":99.651,"s":[0],"e":[0]},{"i":{"x":[0.667],"y":[0.667]},"o":{"x":[0.333],"y":[0.333]},"n":["0p667_0p667_0p333_0p333"],"t":116.574,"s":[0],"e":[0]},{"t":131.6162109375}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[235,235,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":0,"s":[100,100,100],"e":[234,234,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":22.562,"s":[234,234,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":41.365,"s":[100,100,100],"e":[234,234,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":62.047,"s":[234,234,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":80.85,"s":[100,100,100],"e":[234,234,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":99.651,"s":[234,234,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0.167]},"n":["0p667_1_0p167_0","0p667_1_0p167_0","0p667_0p667_0p167_0p167"],"t":116.574,"s":[100,100,100],"e":[234,234,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":131.616,"s":[234,234,100],"e":[99,99,100]},{"t":148.5380859375}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[198.609,198.609]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2833,0,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[1,1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":116,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 15c8a1a..690e8e7 100755 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project - platform :ios, '14.0' + platform :ios, '15.5' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a28512d..d7c6e6c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,29 +1,118 @@ PODS: - Flutter (1.0.0) - - flutter_contacts (0.0.1): - - Flutter - fluttertoast (0.0.2): - Flutter - - Google-Maps-iOS-Utils (5.0.0): - - GoogleMaps (~> 8.0) + - Google-Maps-iOS-Utils (6.1.0): + - GoogleMaps (~> 9.0) - google_maps_flutter_ios (0.0.1): - Flutter - Google-Maps-iOS-Utils (< 7.0, >= 5.0) - GoogleMaps (< 10.0, >= 8.4) - - GoogleMaps (8.4.0): - - GoogleMaps/Maps (= 8.4.0) - - GoogleMaps/Base (8.4.0) - - GoogleMaps/Maps (8.4.0): - - GoogleMaps/Base + - google_mlkit_commons (0.8.1): + - Flutter + - MLKitVision + - google_mlkit_object_detection (0.13.1): + - Flutter + - google_mlkit_commons + - GoogleMLKit/ObjectDetection (~> 6.0.0) + - GoogleMLKit/ObjectDetectionCustom (~> 6.0.0) + - google_mlkit_text_recognition (0.13.1): + - Flutter + - google_mlkit_commons + - GoogleMLKit/TextRecognition (~> 6.0.0) + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleMaps (9.4.0): + - GoogleMaps/Maps (= 9.4.0) + - GoogleMaps/Maps (9.4.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleMLKit/ObjectDetection (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitObjectDetection (~> 5.0.0) + - GoogleMLKit/ObjectDetectionCustom (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitObjectDetectionCustom (~> 5.0.0) + - GoogleMLKit/TextRecognition (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitTextRecognition (~> 4.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Logger + - GTMSessionFetcher/Core (3.5.0) + - home_widget (0.0.1): + - Flutter - location (0.0.1): - Flutter + - MLImage (1.0.0-beta5) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) + - GoogleUtilitiesComponents (~> 1.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitImageLabelingCommon (7.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitObjectDetection (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitObjectDetectionCommon (~> 7.0) + - MLKitVision (~> 7.0) + - MLKitVisionKit (~> 8.0) + - MLKitObjectDetectionCommon (7.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitObjectDetectionCustom (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitObjectDetectionCommon (~> 7.0) + - MLKitVision (~> 7.0) + - MLKitVisionKit (~> 8.0) + - MLKitTextRecognition (4.0.0): + - MLKitCommon (~> 11.0) + - MLKitTextRecognitionCommon (= 3.0.0) + - MLKitVision (~> 7.0) + - MLKitTextRecognitionCommon (3.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - MLKitVisionKit (8.0.0): + - MLKitCommon (~> 11.0) + - MLKitImageLabelingCommon (~> 7.0) + - MLKitObjectDetectionCommon (~> 7.0) + - MLKitVision (~> 7.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - package_info_plus (0.4.5): - Flutter - permission_handler_apple (9.3.0): - Flutter - - razorpay-core-pod (1.0.6) - - razorpay-pod (1.5.3): - - razorpay-core-pod (= 1.0.6) + - PromisesObjC (2.4.1) + - razorpay-pod (1.5.4) - razorpay_flutter (1.1.10): - Flutter - razorpay-pod @@ -32,9 +121,12 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) + - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) + - google_mlkit_object_detection (from `.symlinks/plugins/google_mlkit_object_detection/ios`) + - google_mlkit_text_recognition (from `.symlinks/plugins/google_mlkit_text_recognition/ios`) + - home_widget (from `.symlinks/plugins/home_widget/ios`) - location (from `.symlinks/plugins/location/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -44,19 +136,42 @@ DEPENDENCIES: SPEC REPOS: trunk: - Google-Maps-iOS-Utils + - GoogleDataTransport - GoogleMaps - - razorpay-core-pod + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GoogleUtilitiesComponents + - GTMSessionFetcher + - MLImage + - MLKitCommon + - MLKitImageLabelingCommon + - MLKitObjectDetection + - MLKitObjectDetectionCommon + - MLKitObjectDetectionCustom + - MLKitTextRecognition + - MLKitTextRecognitionCommon + - MLKitVision + - MLKitVisionKit + - nanopb + - PromisesObjC - razorpay-pod EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_contacts: - :path: ".symlinks/plugins/flutter_contacts/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" google_maps_flutter_ios: :path: ".symlinks/plugins/google_maps_flutter_ios/ios" + google_mlkit_commons: + :path: ".symlinks/plugins/google_mlkit_commons/ios" + google_mlkit_object_detection: + :path: ".symlinks/plugins/google_mlkit_object_detection/ios" + google_mlkit_text_recognition: + :path: ".symlinks/plugins/google_mlkit_text_recognition/ios" + home_widget: + :path: ".symlinks/plugins/home_widget/ios" location: :path: ".symlinks/plugins/location/ios" package_info_plus: @@ -70,19 +185,39 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_contacts: 5383945387e7ca37cf963d4be57c21f2fc15ca9f fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 - Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 + Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 - GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d + google_mlkit_commons: 414fc36ac51758c806381de33a1d148d2ba71ce4 + google_mlkit_object_detection: 888a9864123f5ef0e0a8c8d3f1d9f6c14393840d + google_mlkit_text_recognition: 6dd0122107954e64667d826da71de686152109fe + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8 + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitImageLabelingCommon: fa802f14c3a321121409f6cbe1df89ba9cf1a00c + MLKitObjectDetection: 9b40359730baceef23322c6d41070da7639ff100 + MLKitObjectDetectionCommon: f3ca8581a0741be6ce609920367316571a280efa + MLKitObjectDetectionCustom: a412a63a743724ae124b07d6c3cc817283da3453 + MLKitTextRecognition: c83c18ad25496f2077f6ec93c5940487ff2eb343 + MLKitTextRecognitionCommon: c0b3a63d529296a19bce1f8bc8a513644ed4d1f6 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + MLKitVisionKit: 6d2ea9741b262ec0fed2a5a3de521cfa671c6e83 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 package_info_plus: 566e1b7a2f3900e4b0020914ad3fc051dcc95596 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - razorpay-core-pod: 6da5fb4ed280279d665507fdb50e213d793a5fe2 - razorpay-pod: 4385cf844aa29389313b741e20c72fe668970d49 + PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273 + razorpay-pod: 3ba80419f3315352d6ffa336a194ad95ab249700 razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a sms_autofill: b36b2147535657fea83d7f3898d7831de70bd8e4 -PODFILE CHECKSUM: a901b0534ebff1a81cfb53fbf723b1bca433de9f +PODFILE CHECKSUM: 5cbaa2e976150d6ca52c31ab6e60efa2670a60bc COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 99fdc20..42b8cd5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,13 +10,13 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 496924C4B012DE93FE3F4ECA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 415548DB485576CB7FE174F9 /* Pods_Runner.framework */; }; - 54D9B915190C58FAD7127809 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 7986347451EEDDA598144EBD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + BF8C3AB8B374BC2E78DCF9F9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -48,14 +48,15 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 415548DB485576CB7FE174F9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5DA0CC029FD23DB3E018EF32 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 6DEB7185559E0DA885F8BBB2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 73038318DDCE8E5C422FFDAC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; - 7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 81CC33AFB41B5982FB30E75D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -63,10 +64,9 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C30F8E9BE0DAA4B0BBDA9B05 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + E941989A32F6914B4203541B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,7 +75,7 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 496924C4B012DE93FE3F4ECA /* Pods_Runner.framework in Frameworks */, + BF8C3AB8B374BC2E78DCF9F9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,7 +83,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 54D9B915190C58FAD7127809 /* Pods_RunnerTests.framework in Frameworks */, + 7986347451EEDDA598144EBD /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -118,7 +118,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, D6A52DD0F305C2721292572B /* Pods */, - A8EB2AA264F77BD42DF91F7E /* Frameworks */, + B515A6DBF5715200D9D48CDA /* Frameworks */, ); sourceTree = ""; }; @@ -146,11 +146,11 @@ path = Runner; sourceTree = ""; }; - A8EB2AA264F77BD42DF91F7E /* Frameworks */ = { + B515A6DBF5715200D9D48CDA /* Frameworks */ = { isa = PBXGroup; children = ( - 415548DB485576CB7FE174F9 /* Pods_Runner.framework */, - F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */, + 7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */, + 42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -158,12 +158,12 @@ D6A52DD0F305C2721292572B /* Pods */ = { isa = PBXGroup; children = ( - 6DEB7185559E0DA885F8BBB2 /* Pods-Runner.debug.xcconfig */, - 5DA0CC029FD23DB3E018EF32 /* Pods-Runner.release.xcconfig */, - C30F8E9BE0DAA4B0BBDA9B05 /* Pods-Runner.profile.xcconfig */, - D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */, - DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */, - 7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */, + 81CC33AFB41B5982FB30E75D /* Pods-Runner.debug.xcconfig */, + E941989A32F6914B4203541B /* Pods-Runner.release.xcconfig */, + 73038318DDCE8E5C422FFDAC /* Pods-Runner.profile.xcconfig */, + FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */, + 48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */, + A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -175,7 +175,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 1D8A7B8D320B90CD1E12A7EF /* [CP] Check Pods Manifest.lock */, + 47F6B4685984264CA6501959 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, D2D8874BAFC912C92F358E77 /* Frameworks */, @@ -194,15 +194,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - EA24EB15C10E42710DBCFB14 /* [CP] Check Pods Manifest.lock */, + C5E75C1082B7CA6F769A3C87 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - D51672018CD7C6B1266BE9E3 /* [CP] Embed Pods Frameworks */, - A7A93A12585D43A616D73706 /* [CP] Copy Pods Resources */, + 83DD7C5BA49A69DFF78E0954 /* [CP] Embed Pods Frameworks */, + 80FE172A180B5CF341443AC5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -280,7 +280,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D8A7B8D320B90CD1E12A7EF /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 47F6B4685984264CA6501959 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -302,38 +318,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - A7A93A12585D43A616D73706 /* [CP] Copy Pods Resources */ = { + 80FE172A180B5CF341443AC5 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -350,7 +335,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - D51672018CD7C6B1266BE9E3 /* [CP] Embed Pods Frameworks */ = { + 83DD7C5BA49A69DFF78E0954 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -367,7 +352,22 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - EA24EB15C10E42710DBCFB14 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C5E75C1082B7CA6F769A3C87 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -515,7 +515,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -533,7 +533,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -549,7 +549,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c10c0ef..88cda11 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "version" : "11.2.0" } }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c", + "version" : "2.2.1" + } + }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index c10c0ef..88cda11 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "version" : "11.2.0" } }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c", + "version" : "2.2.1" + } + }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 94eb8f0..0eb2b32 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -34,29 +34,10 @@ Required for voice-based payment authentication NSPhotoLibraryUsageDescription Required to upload payment screenshots - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - FlutterSceneDelegate - UISceneStoryboardFile - Main - - - - UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/controllers/authentication/auth_controller.dart b/lib/controllers/authentication/auth_controller.dart index f14f0ef..f06fcba 100644 --- a/lib/controllers/authentication/auth_controller.dart +++ b/lib/controllers/authentication/auth_controller.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; import 'package:dio/dio.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -14,16 +15,20 @@ import '../../constants/error_constants.dart'; import '../../data/authentication/auth_request.dart'; import '../../data/authentication/auth_response.dart'; import '../../domain/repository/authentication/auth_repository.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; import '../../view/authentication/costomer_create_view.dart'; import '../../view/authentication/verification_view.dart'; import '../../view/dashboard_view/dashboard_view.dart'; import '../../view/home_view.dart'; -import '../tenant_controller /tenant_list.dart'; +import '../tenant_list/tenant_controller.dart'; class AuthController extends GetxController { + + final FirebaseAnalytics analytics = FirebaseAnalytics.instance; LoginRepository loginRepository = LoginRepository(); final TenantController tenantControllers = Get.put(TenantController()); + var isLoading = false.obs; int? activeStatus; @@ -129,6 +134,16 @@ class AuthController extends GetxController { ErrorConstants.apiError.value = false; + await AnalyticsService.logEvent( + 'login_success', + parameters: { + 'customer_id': customerId.toString(), + 'name': result?.details?.firstname ?? 'unknown', + 'email': result?.details?.email ?? 'unknown', + 'city': result?.details?.suburb ?? 'unknown', + }, + ); + isNewUser=false; validateDevice(deviceId ?? ''); diff --git a/lib/controllers/cart_controller/cart.dart b/lib/controllers/cart_controller/cart.dart index 8ca79d5..c87c0a2 100644 --- a/lib/controllers/cart_controller/cart.dart +++ b/lib/controllers/cart_controller/cart.dart @@ -10,7 +10,7 @@ import '../../modules/authentication/auth.dart'; import '../../modules/product/product.dart'; import '../../modules/tenant/get_tenant.dart' hide Customer; import '../../service/dio.dart'; -import '../tenant_controller /tenant_list.dart'; // New Product modules +import '../tenant_list/tenant_controller.dart'; class CartController extends GetxController { var cartItems = [].obs; @@ -606,6 +606,7 @@ class CartController extends GetxController { } + /// Clear cart void clearCart() => cartItems.clear(); @@ -617,6 +618,8 @@ class CartController extends GetxController { cartItems.fold(0, (sum, item) => sum + ((item.product.productcost ?? 0) * item.quantity)); } + + /// Cart item class class CartItem { final Product product; diff --git a/lib/controllers/dashboard_controller/dashboard_controller.dart b/lib/controllers/dashboard_controller/dashboard_controller.dart index 4ec44ff..f857e33 100644 --- a/lib/controllers/dashboard_controller/dashboard_controller.dart +++ b/lib/controllers/dashboard_controller/dashboard_controller.dart @@ -22,7 +22,7 @@ import '../../modules/tenant/category.dart'; import '../../modules/tenant/get_tenant.dart'; import '../../view/authentication/costomer_create_view.dart'; import '../../widgets/text_widget.dart'; -import '../tenant_controller /tenant_list.dart'; +import '../tenant_list/tenant_controller.dart'; class DashboardController extends GetxController { // Loading state diff --git a/lib/controllers/order_controller/create_order_controller.dart b/lib/controllers/order_controller/create_order_controller.dart index 3fcd1fd..e96105a 100644 --- a/lib/controllers/order_controller/create_order_controller.dart +++ b/lib/controllers/order_controller/create_order_controller.dart @@ -7,6 +7,7 @@ import '../../modules/orders/create_order.dart'; class OrderController extends GetxController { final CreateOrderProvider provider = CreateOrderProvider(); var isLoading = false.obs; + String? lastError; // 👈 add this Future createOrder(CreateOrderRequest request) async { try { @@ -17,20 +18,19 @@ class OrderController extends GetxController { isLoading.value = false; if (response.status == 'accepted') { - print(response.status); - print("✅ Order Success"); } else { print("❌ Order Failed"); } - return response; // ✅ VERY IMPORTANT + return response; } catch (e) { isLoading.value = false; - print("🔥 ERROR: $e"); + print("🔥 ERROR: $e"); // already printing e here + print("🔥 ERROR TYPE: ${e.runtimeType}"); // 👈 add this to see the exact class - return null; // ✅ return null on error + return null; } }} diff --git a/lib/controllers/search_controller/GrocerySearchController.dart b/lib/controllers/search_controller/GrocerySearchController.dart new file mode 100644 index 0000000..f4ad877 --- /dev/null +++ b/lib/controllers/search_controller/GrocerySearchController.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +import '../../service/ai_service/api_service.dart'; + +class AiSearchController extends GetxController { + final AiSearchService service = AiSearchService(); + + RxBool loading = false.obs; + RxMap result = {}.obs; + + Future search(String query) async { + try { + loading.value = true; + + final data = await service.search(query); + + result.value = data; + } catch (e) { + Get.snackbar( + "Error", + e.toString(), + ); + } finally { + loading.value = false; + } + } +} \ No newline at end of file diff --git a/lib/controllers/search_controller/product_match.dart b/lib/controllers/search_controller/product_match.dart new file mode 100644 index 0000000..ea72317 --- /dev/null +++ b/lib/controllers/search_controller/product_match.dart @@ -0,0 +1,485 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import '../../modules/search/grocery_item.dart'; + +// ─── Models ─────────────────────────────────────────────────────────────────── + +class StoreProduct { + final int productId; + final String productName; + final String productImage; + final double productCost; + final double discountValue; + final String productUnit; + final String unitValue; + final String productStatus; + final int subcategoryId; + final String subcategoryName; + + const StoreProduct({ + required this.productId, + required this.productName, + required this.productImage, + required this.productCost, + required this.discountValue, + required this.productUnit, + required this.unitValue, + required this.productStatus, + required this.subcategoryId, + required this.subcategoryName, + }); + + factory StoreProduct.fromJson(Map json, String subcategoryName) { + return StoreProduct( + productId: json['productid'] ?? 0, + productName: json['productname'] ?? '', + productImage: json['productimage'] ?? '', + productCost: (json['productcost'] ?? 0).toDouble(), + discountValue: (json['discountvalue'] ?? 0).toDouble(), + productUnit: json['productunit'] ?? '', + unitValue: json['unitvalue'] ?? '', + productStatus: json['productstatus'] ?? '', + subcategoryId: json['subcategoryid'] ?? 0, + subcategoryName: subcategoryName, + ); + } + + bool get isAvailable => productStatus == 'available'; + + double get finalPrice { + if (discountValue <= 0) return productCost; + return productCost - (productCost * discountValue / 100); + } +} + +class StoreInfo { + final int tenantId; + final String tenantName; + final String address; + final String suburb; + final String city; + final String latitude; + final String longitude; + final int locationId; + final String locationName; + final int categoryId; + final int ordersCount; + + const StoreInfo({ + required this.tenantId, + required this.tenantName, + required this.address, + required this.suburb, + required this.city, + required this.latitude, + required this.longitude, + required this.locationId, + required this.locationName, + required this.categoryId, + required this.ordersCount, + }); + + factory StoreInfo.fromJson(Map json) { + return StoreInfo( + tenantId: json['tenantid'] ?? 0, + tenantName: json['tenantname'] ?? '', + address: json['address'] ?? '', + suburb: json['suburb'] ?? '', + city: json['city'] ?? '', + latitude: json['latitude'] ?? '', + longitude: json['longitude'] ?? '', + locationId: json['locationid'] ?? 0, + locationName: json['locationname'] ?? '', + categoryId: json['categoryid'] ?? 0, + ordersCount: json['orderscount'] ?? 0, + ); + } +} + +/// A single AI item matched to one or more store products across stores. +class MatchedItem { + final GroceryItem aiItem; + + /// storeName → list of matched products in that store + final Map> storeMatches; + + const MatchedItem({ + required this.aiItem, + required this.storeMatches, + }); + + bool get hasMatches => storeMatches.isNotEmpty; + + int get totalMatchCount => + storeMatches.values.fold(0, (sum, list) => sum + list.length); +} + +// ─── Controller ─────────────────────────────────────────────────────────────── + +class StoreMatchController extends ChangeNotifier { + // ── Config ───────────────────────────────────────────────────────────────── + + static const int _customerId = 6060; + static const double _latitude = 11.0050728; + static const double _longitude = 76.9508513; + + static const String _baseUrl = 'https://fiesta.nearle.app/live/api/v1/mob'; + static const String _tenantsEndpoint = '$_baseUrl/tenants/getcustomertenants'; + static const String _productsEndpoint = '$_baseUrl/products/getproductsbysubcategory'; + + static const String _groqApiKey = + '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'; + static const String _groqUrl = + 'https://api.groq.com/openai/v1/chat/completions'; + + // ── State ────────────────────────────────────────────────────────────────── + + bool loading = false; + String? error; + + List stores = []; + final Map> _storeProducts = {}; + List matchedResults = []; + + // ── Public entry point ──────────────────────────────────────────────────── + + Future matchAiItems(List aiItems) async { + if (aiItems.isEmpty) return; + + loading = true; + error = null; + matchedResults = []; + notifyListeners(); + + try { + // Step 1 — fetch all nearby stores + await _fetchStores(); + + if (stores.isEmpty) { + error = 'No stores found nearby.'; + loading = false; + notifyListeners(); + return; + } + + // Step 2 — fetch products for every store concurrently + await _fetchAllStoreProducts(); + + // Step 3 — AI matching + await _performAiMatching(aiItems); + + // Step 4 — print results + _printMatchResults(); + + } catch (e, st) { + error = 'Something went wrong: $e'; + debugPrint('[StoreMatch] Error: $e\n$st'); + } finally { + loading = false; + notifyListeners(); + } + } + + // ── Step 1: Fetch stores ────────────────────────────────────────────────── + + Future _fetchStores() async { + final uri = Uri.parse(_tenantsEndpoint).replace(queryParameters: { + 'customerid': '$_customerId', + 'tenant': '0', + 'latitude': '$_latitude', + 'longitude': '$_longitude', + 'categoryid': '0', + }); + + debugPrint('[StoreMatch] Fetching stores → $uri'); + + final res = await http.get(uri); + if (res.statusCode != 200) { + throw Exception('Stores API error ${res.statusCode}'); + } + + final json = jsonDecode(res.body) as Map; + if (json['status'] != true) { + throw Exception('Stores API returned status=false: ${json['message']}'); + } + + final details = (json['details'] as List?) ?? []; + + final seen = {}; + stores = details + .map((d) => StoreInfo.fromJson(d as Map)) + .where((s) { + final key = '${s.tenantId}_${s.locationId}'; + return seen.add(key); + }).toList(); + + debugPrint('[StoreMatch] Fetched ${stores.length} store-location(s).'); + } + + // ── Step 2: Fetch products for every store ──────────────────────────────── + + Future _fetchAllStoreProducts() async { + await Future.wait(stores.map(_fetchProductsForStore)); + } + + Future _fetchProductsForStore(StoreInfo store) async { + final storeKey = '${store.tenantId}_${store.locationId}'; + + final uri = Uri.parse(_productsEndpoint).replace(queryParameters: { + 'categoryid': '${store.categoryId == 0 ? 2 : store.categoryId}', + 'tenantid': '${store.tenantId}', + 'locationid': '${store.locationId}', + }); + + debugPrint('[StoreMatch] Fetching products for ${store.tenantName} → $uri'); + + try { + final res = await http.get(uri); + if (res.statusCode != 200) return; + + final json = jsonDecode(res.body) as Map; + if (json['status'] != true || json['data'] == null) return; + + final details = (json['data']['details'] as List?) ?? []; + + final products = []; + for (final cat in details) { + final subcatName = (cat['subcategoryname'] as String?) ?? ''; + final productList = (cat['products'] as List?) ?? []; + for (final p in productList) { + products.add( + StoreProduct.fromJson(p as Map, subcatName), + ); + } + } + + _storeProducts[storeKey] = products; + debugPrint( + '[StoreMatch] ${store.tenantName} (${store.locationName}): ' + '${products.length} products loaded.', + ); + } catch (e) { + debugPrint('[StoreMatch] Failed to fetch products for $storeKey: $e'); + } + } + + // ── Step 3: AI matching ─────────────────────────────────────────────────── + + Future _performAiMatching(List aiItems) async { + // Build the AI item list string + final aiItemsList = aiItems + .map((item) => '- ${item.name} (${item.quantity})') + .join('\n'); + + // Build the store products list string + // Format: "storeName — locationName|productName" + final productLines = []; + for (final store in stores) { + final storeKey = '${store.tenantId}_${store.locationId}'; + final products = _storeProducts[storeKey] ?? []; + final storeLabel = '${store.tenantName} — ${store.locationName}'; + for (final p in products) { + productLines.add('$storeLabel|${p.productName}'); + } + } + + if (productLines.isEmpty) { + debugPrint('[StoreMatch] No store products to match against.'); + matchedResults = aiItems + .map((item) => MatchedItem(aiItem: item, storeMatches: {})) + .toList(); + return; + } + + final productListString = productLines.join('\n'); + + debugPrint('[StoreMatch] Sending ${aiItems.length} AI items and ' + '${productLines.length} store products to Groq for matching...'); + + final prompt = ''' +You are a strict grocery product matching engine. + +RULES: +- Match each AI item ONLY to products whose name is genuinely the same item. +- "Chicken Biryani" must ONLY match products containing "biryani" or "chicken biryani". +- "Curd" must NEVER match "Biryani". "Eggs" must NEVER match "Rice". +- Be flexible with brand names: "Basmati Rice" matches "India Gate Basmati Rice 1kg". +- If you are not confident, return an empty matches array. +- The "matches" array must contain EXACT product names copied from the STORE PRODUCTS list below. Do NOT paraphrase or modify them. +- Return ONLY a raw JSON array. No markdown, no explanation, nothing else. + +Each element must have: +- "aiItem": the exact AI item name from the list below +- "matches": array of exact product name strings from the store products list + +═══════════════════════ +AI ITEMS (user is looking for): +$aiItemsList + +═══════════════════════ +STORE PRODUCTS (format: storeName|productName): +$productListString +═══════════════════════ + +Return format example: +[ + {"aiItem": "Chicken Biryani", "matches": ["Chicken Biryani (Full)", "Biryani Chicken"]}, + {"aiItem": "Eggs", "matches": ["Farm Fresh Eggs 6pcs"]}, + {"aiItem": "Pizza", "matches": []} +] +'''; + + try { + final res = await http.post( + Uri.parse(_groqUrl), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $_groqApiKey', + }, + body: jsonEncode({ + 'model': 'llama-3.3-70b-versatile', + 'max_tokens': 1000, + 'messages': [ + {'role': 'user', 'content': prompt}, + ], + }), + ); + + if (res.statusCode != 200) { + throw Exception('Groq matching API error ${res.statusCode}: ${res.body}'); + } + + final data = jsonDecode(res.body) as Map; + final content = data['choices'][0]['message']['content'] as String; + + // Extract JSON array from response + final start = content.indexOf('['); + final end = content.lastIndexOf(']') + 1; + if (start == -1 || end == 0) { + throw Exception('No JSON array in Groq matching response'); + } + + final List parsed = jsonDecode(content.substring(start, end)) as List; + + // Build productName (lowercase) → (storeLabel, StoreProduct) lookup + // Key: "storeName — locationName|productname_lowercase" + // This avoids cross-store collisions if two stores have the same product name + final productLookup = >{}; + for (final store in stores) { + final storeKey = '${store.tenantId}_${store.locationId}'; + final products = _storeProducts[storeKey] ?? []; + final storeLabel = '${store.tenantName} — ${store.locationName}'; + for (final p in products) { + final lookupKey = '$storeLabel|${p.productName.toLowerCase().trim()}'; + productLookup[lookupKey] = MapEntry(storeLabel, p); + } + } + + // Build MatchedItem list from AI response + matchedResults = aiItems.map((aiItem) { + final entry = parsed.firstWhere( + (e) => + (e['aiItem'] as String).toLowerCase().trim() == + aiItem.name.toLowerCase().trim(), + orElse: () => null, + ); + + if (entry == null) { + return MatchedItem(aiItem: aiItem, storeMatches: {}); + } + + final matchedNames = ((entry['matches'] as List?) ?? []) + .map((e) => (e as String).trim()) + .toList(); + + // Group matched products by store name + final storeMatches = >{}; + + for (final matchedName in matchedNames) { + final matchedNameLower = matchedName.toLowerCase(); + + // Search across all stores for this product name + for (final store in stores) { + final storeKey = '${store.tenantId}_${store.locationId}'; + final storeLabel = '${store.tenantName} — ${store.locationName}'; + final lookupKey = '$storeLabel|$matchedNameLower'; + + final found = productLookup[lookupKey]; + if (found != null) { + storeMatches + .putIfAbsent(found.key, () => []) + .add(found.value); + } + } + } + + return MatchedItem(aiItem: aiItem, storeMatches: storeMatches); + }).toList(); + + } catch (e, st) { + debugPrint('[StoreMatch] AI matching failed: $e\n$st'); + // Fallback: return all items with no matches rather than crashing + matchedResults = aiItems + .map((item) => MatchedItem(aiItem: item, storeMatches: {})) + .toList(); + } + } + + // ── Step 4: Print results ───────────────────────────────────────────────── + + void _printMatchResults() { + debugPrint('\n${'═' * 60}'); + debugPrint(' AI ITEM → STORE MATCH RESULTS (AI-powered)'); + debugPrint('${'═' * 60}'); + + for (final result in matchedResults) { + debugPrint('\n▶ AI Item: "${result.aiItem.name}" ' + '(${result.aiItem.quantity}) ${result.aiItem.emoji}'); + + if (!result.hasMatches) { + debugPrint(' ✗ No matching products found in any store.'); + continue; + } + + result.storeMatches.forEach((storeName, products) { + debugPrint(' ✔ $storeName'); + for (final p in products) { + final discount = p.discountValue > 0 + ? ' [${p.discountValue.toStringAsFixed(0)}% off → ₹${p.finalPrice.toStringAsFixed(2)}]' + : ''; + final status = p.isAvailable ? '✅' : '⚠ outofstock'; + debugPrint( + ' • ${p.productName} | ₹${p.productCost}' + '$discount | ${p.productUnit} | $status', + ); + } + }); + } + + debugPrint('\n${'═' * 60}'); + final matchedCount = matchedResults.where((r) => r.hasMatches).length; + debugPrint( + ' SUMMARY: ${matchedCount}/${matchedResults.length} AI items matched ' + 'across ${stores.length} stores.', + ); + debugPrint('${'═' * 60}\n'); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + void reset() { + stores = []; + _storeProducts.clear(); + matchedResults = []; + error = null; + loading = false; + notifyListeners(); + } + + List get foundItems => + matchedResults.where((r) => r.hasMatches).toList(); + + List get notFoundItems => + matchedResults.where((r) => !r.hasMatches).toList(); +} \ No newline at end of file diff --git a/lib/controllers/search_controller/search_controller.dart b/lib/controllers/search_controller/search_controller.dart new file mode 100644 index 0000000..d0c168b --- /dev/null +++ b/lib/controllers/search_controller/search_controller.dart @@ -0,0 +1,295 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:nearledaily/controllers/search_controller/product_match.dart'; +import '../../modules/search/grocery_item.dart'; + +class GrocerySearchController extends ChangeNotifier { + // ─── Controllers & Focus ────────────────────────────────────────────────── + + final TextEditingController textController = TextEditingController(); + final FocusNode focusNode = FocusNode(); + + // ─── Animation ──────────────────────────────────────────────────────────── + + late AnimationController cartBarController; + late Animation cartBarSlide; + + // ─── State ──────────────────────────────────────────────────────────────── + + List items = []; + List cartItems = []; + bool loading = false; + bool hasSearched = false; + String lastQuery = ''; + + // ─── API ────────────────────────────────────────────────────────────────── + + static const String _apiKey = + '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'; + + // ─── Quick Chips ────────────────────────────────────────────────────────── + + final List> quickChips = [ + {'label': '🍛 Biryani', 'query': 'Biryani'}, + {'label': '🍝 Pasta', 'query': 'Pasta'}, + {'label': '🎂 Cake', 'query': 'Cake'}, + {'label': '🥞 Pancakes', 'query': 'Pancakes'}, + {'label': '🫘 Dal Tadka', 'query': 'Dal Tadka'}, + {'label': '🥘 Idli Sambar', 'query': 'Idli Sambar'}, + {'label': '🍕 Pizza', 'query': 'Pizza'}, + {'label': '🍜 Noodles', 'query': 'Noodles'}, + ]; + + final List> naturalChips = [ + { + 'label': '🤧 I have a cold — what should I eat?', + 'query': 'I have a cold, what fruits or foods should I eat?' + }, + { + 'label': '💪 Post-workout nutrition', + 'query': 'Post-workout recovery foods and snacks' + }, + { + 'label': '🍽️ Biryani + Curd Rice together', + 'query': 'I want to make biryani and also curd rice' + }, + { + 'label': '🍳 Quick egg breakfast', + 'query': 'Quick breakfast with eggs and bread' + }, + ]; + + // ─── Init / Dispose ─────────────────────────────────────────────────────── + + void init(TickerProvider vsync) { + // Register StoreMatchController if not already registered + if (!Get.isRegistered()) { + Get.put(StoreMatchController()); + } + initAnimations(vsync); + } + + void initAnimations(TickerProvider vsync) { + cartBarController = AnimationController( + vsync: vsync, + duration: const Duration(milliseconds: 300), + ); + cartBarSlide = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: cartBarController, + curve: Curves.easeOutCubic, + )); + } + + void disposeAll() { + textController.dispose(); + focusNode.dispose(); + cartBarController.dispose(); + } + + // ─── Search ─────────────────────────────────────────────────────────────── + + Future search(String query) async { + final q = query.trim(); + if (q.isEmpty) return; + + focusNode.unfocus(); + loading = true; + hasSearched = true; + lastQuery = q; + items = []; + notifyListeners(); + + try { + final res = await http.post( + Uri.parse('https://api.groq.com/openai/v1/chat/completions'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $_apiKey', + }, + body: jsonEncode({ + 'model': 'llama-3.1-8b-instant', + 'max_tokens': 800, + 'messages': [ + { + 'role': 'system', + 'content': '''You are a smart product search engine for a super-app with grocery stores, restaurants, pharmacies, and general stores. + +Your ONLY job is to return a raw JSON array. No markdown, no code fences, no explanations. Raw JSON only. + +═══════════════════════════════════════════ +GOLDEN RULE — LITERAL FIRST: +If the user types a RAW INGREDIENT, PRODUCT, or ITEM NAME (e.g. "tomato", "milk", "onion", "shampoo", "paracetamol"), +return THAT EXACT ITEM as a grocery/store product. Do NOT transform it into a dish or recipe. +═══════════════════════════════════════════ + +INTENT DETECTION — pick ONE mode: + +MODE 1 — DIRECT PRODUCT SEARCH (user types a product/ingredient name): + Signals: single word or short phrase that IS a product (e.g. "tomato", "milk", "bread", "eggs", "rice") + Action: Return that exact product with 2–4 size/variant options only. + Do NOT suggest dishes made from it. + type: "grocery" + Example — query "tomato": + [ + {"name": "Tomato", "quantity": "500g", "emoji": "🍅", "type": "grocery"}, + {"name": "Tomato", "quantity": "1 kg", "emoji": "🍅", "type": "grocery"}, + {"name": "Cherry Tomato", "quantity": "250g", "emoji": "🍅", "type": "grocery"} + ] + +MODE 2 — RESTAURANT ORDER (user wants a ready-made dish delivered): + Signals: dish name WITH food-order context, or words like "order", "hungry", "eat", + "food delivery", "i want to eat [dish]", "get me [dish]" + Action: Return the dish as 1–3 variants. + type: "restaurant" + Example — query "i want biryani": + [ + {"name": "Chicken Biryani", "quantity": "1 portion", "emoji": "🍛", "type": "restaurant"}, + {"name": "Veg Biryani", "quantity": "1 portion", "emoji": "🍚", "type": "restaurant"} + ] + +MODE 3 — COOK AT HOME (user wants to cook): + Signals: "cook", "recipe", "ingredients for", "how to make", "groceries for", "i will make" + Action: Return grocery ingredient list (6–12 items). + type: "grocery" + +MODE 4 — SITUATION / NEED (health, mood, lifestyle): + Signals: "sick", "fever", "diet", "weight loss", "party", "snacks", "breakfast", "workout" + Action: Return 5–10 relevant products. Mix types if appropriate. + +═══════════════════════════════════════════ +STRICT ACCURACY RULES: +- A product name is a product — never auto-upgrade it to a dish +- Only return items the user actually needs — no upsells, no extras +- If query is ambiguous between product vs dish, choose MODE 1 (product) +- "tomato" → tomatoes, NOT tomato rice or tomato sauce +- "chicken" → raw chicken cuts, NOT chicken curry or chicken burger +- "lemon" → lemons, NOT lemon juice or lemon cake +═══════════════════════════════════════════ + +OUTPUT RULES: +- Raw JSON array only. Nothing else. +- Every item: exactly 4 keys — "name", "quantity", "emoji", "type" +- "type": only "restaurant" or "grocery" +- No duplicates. No nulls. No empty strings. +- Max 6 items total unless MODE 3 (ingredients list).''', + }, + {'role': 'user', 'content': q}, + ], + }), + ); + + if (res.statusCode != 200) { + throw Exception('API error ${res.statusCode}: ${res.body}'); + } + + final data = jsonDecode(res.body) as Map; + final content = data['choices'][0]['message']['content'] as String; + + final start = content.indexOf('['); + final end = content.lastIndexOf(']') + 1; + if (start == -1 || end == 0) { + throw Exception('No JSON array found in response'); + } + + final List parsed = + jsonDecode(content.substring(start, end)) as List; + + final newItems = parsed + .asMap() + .entries + .map((e) => + GroceryItem.fromJson(e.key, e.value as Map)) + .toList(); + + // Preserve cart state for already-added items + for (final item in newItems) { + if (cartItems.any((c) => c.name == item.name)) { + item.inCart = true; + } + } + + items = newItems; + + // ── Trigger store matching ────────────────────────────────────────── + if (items.isNotEmpty) { + try { + final storeCtrl = Get.find(); + await storeCtrl.matchAiItems(items); + } catch (e, st) { + print('[StoreMatch] Error during matching: $e'); + print(' StackTrace: $st'); + } + } + + } on FormatException catch (e) { + print('[GrocerySearch] FormatException: $e'); + } on http.ClientException catch (e) { + print('[GrocerySearch] Network error: $e'); + } catch (e, st) { + print('[GrocerySearch] Unexpected error: $e'); + print(' StackTrace: $st'); + } finally { + loading = false; + notifyListeners(); + } + } + // ─── Cart ───────────────────────────────────────────────────────────────── + + void toggleCart(GroceryItem item) { + HapticFeedback.lightImpact(); + if (item.inCart) { + item.inCart = false; + cartItems.removeWhere((c) => c.id == item.id && c.name == item.name); + } else { + item.inCart = true; + cartItems.add(item); + } + cartItems.isNotEmpty + ? cartBarController.forward() + : cartBarController.reverse(); + notifyListeners(); + } + + void addAllToCart() { + HapticFeedback.mediumImpact(); + for (final item in items) { + if (!item.inCart) { + item.inCart = true; + cartItems.add(item); + } + } + cartBarController.forward(); + notifyListeners(); + } + + bool get allInCart => items.isNotEmpty && items.every((i) => i.inCart); + + void removeFromCart(GroceryItem item) { + item.inCart = false; + cartItems.removeWhere((c) => c.id == item.id && c.name == item.name); + if (cartItems.isEmpty) cartBarController.reverse(); + notifyListeners(); + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + void clearSearch() { + textController.clear(); + hasSearched = false; + items = []; + lastQuery = ''; + focusNode.requestFocus(); + notifyListeners(); + } + + void fillSearch(String query) { + textController.text = query; + search(query); + } +} \ No newline at end of file diff --git a/lib/controllers/tenant_controller /tenant_list.dart b/lib/controllers/tenant_list/tenant_controller.dart similarity index 100% rename from lib/controllers/tenant_controller /tenant_list.dart rename to lib/controllers/tenant_list/tenant_controller.dart diff --git a/lib/core/services/ExitWrapper.dart b/lib/core/services/ExitWrapper.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/services/vector_service.dart b/lib/core/services/vector_service.dart new file mode 100644 index 0000000..9f4ff43 --- /dev/null +++ b/lib/core/services/vector_service.dart @@ -0,0 +1,86 @@ +import 'dart:io'; +import 'dart:math' as math; + +import 'package:image/image.dart' as img; + +/// Holds the two vector embeddings: one for the cropped product image, +/// one for the OCR-extracted label text. +class VectorEmbeddingResult { + final List imageVector; + final List textVector; + + const VectorEmbeddingResult({ + required this.imageVector, + required this.textVector, + }); +} + +/// Computes vector embeddings entirely on-device — no backend call. +/// +/// NOTE: these are lightweight placeholder embeddings (a downsampled +/// grayscale pixel vector for the image, a character-hashing vector for +/// the text), NOT a trained model like CLIP. They're deterministic and +/// good enough to wire up printing/plumbing now. Swap `_imageVector` / +/// `_textVector` for a real on-device model (e.g. a TFLite feature +/// extractor) or a backend call later without touching the call site. +class VectorService { + static const int _imageVectorSize = 64; // 8x8 downsampled grayscale + static const int _textVectorSize = 32; // hashed char buckets + + static Future getEmbeddings({ + required String imagePath, + required String labelText, + }) async { + final imageVector = await _imageVector(imagePath); + final textVector = _textVector(labelText); + + return VectorEmbeddingResult( + imageVector: imageVector, + textVector: textVector, + ); + } + + /// Downsamples the image to an 8x8 grayscale grid and flattens it into + /// a normalized (0-1) vector of length [_imageVectorSize]. + static Future> _imageVector(String imagePath) async { + final bytes = await File(imagePath).readAsBytes(); + final decoded = img.decodeImage(bytes); + if (decoded == null) { + return List.filled(_imageVectorSize, 0); + } + + final side = math.sqrt(_imageVectorSize).round(); // 8 + final resized = img.copyResize(decoded, width: side, height: side); + final gray = img.grayscale(resized); + + final vector = []; + for (var y = 0; y < side; y++) { + for (var x = 0; x < side; x++) { + final pixel = gray.getPixel(x, y); + vector.add(pixel.r / 255.0); // grayscale => r == g == b + } + } + return vector; + } + + /// Simple character-hashing bag-of-characters vector, normalized so + /// values sum to 1 (empty text => all zeros). + static List _textVector(String text) { + final vector = List.filled(_textVectorSize, 0); + final normalized = text.toLowerCase(); + if (normalized.isEmpty) return vector; + + for (final rune in normalized.runes) { + final bucket = rune % _textVectorSize; + vector[bucket] += 1; + } + + final total = vector.fold(0, (sum, v) => sum + v); + if (total > 0) { + for (var i = 0; i < vector.length; i++) { + vector[i] = vector[i] / total; + } + } + return vector; + } +} \ No newline at end of file diff --git a/lib/domain/provider/authentication/auth_provider.dart b/lib/domain/provider/authentication/auth_provider.dart index 74aa605..76bdcf7 100644 --- a/lib/domain/provider/authentication/auth_provider.dart +++ b/lib/domain/provider/authentication/auth_provider.dart @@ -68,8 +68,10 @@ class LoginProvider { CustomerFullView? profile; final customDio = CustomDio(); final url = "${ApiConstants.fetchProfile}customerid=$customerId&contactno=''"; + try { + print(url); final response = await customDio.getData(url, headers: { 'x-hasura-admin-secret': 'nearle-admin-secret', diff --git a/lib/features/Products/product_card.dart b/lib/features/Products/product_card.dart new file mode 100644 index 0000000..2b1b7bc --- /dev/null +++ b/lib/features/Products/product_card.dart @@ -0,0 +1,316 @@ + + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../modules/search_model/search_model.dart'; +import '../../widgets/fav_button.dart'; + +class ProductCard extends StatefulWidget { + final ProductItem item; + final VoidCallback? onAddTap; + final VoidCallback? onIncrementTap; + final VoidCallback? onDecrementTap; + final int quantity; // 0 = show "Add", >0 = show stepper + + const ProductCard({ + super.key, + required this.item, + this.onAddTap, + this.onIncrementTap, + this.onDecrementTap, + this.quantity = 0, + }); + + @override + State createState() => _ProductCardState(); +} + +class _ProductCardState extends State { + bool _isFav = false; + + @override + void initState() { + super.initState(); + _isFav = widget.item.isFavorite; + } + + @override + Widget build(BuildContext context) { + final item = widget.item; + + final original = double.tryParse( + item.originalPrice.replaceAll(RegExp(r'[^\d.]'), ''), + ) ?? + 0; + final current = double.tryParse( + item.price.replaceAll(RegExp(r'[^\d.]'), ''), + ) ?? + 0; + final discount = original > 0 + ? ((original - current) / original * 100).round() + : 0; + + return Container( + decoration: BoxDecoration( + + borderRadius: BorderRadius.circular(20), + + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // ── Image area ── + Stack( + children: [ + + // Product image + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20)), + child: CachedNetworkImage( + imageUrl: item.image, + height: 100, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (context, url, error) => Container( + height: 140, + color: const Color(0xFFF3F0FF), + child: Image.network('https://i0.wp.com/www.chitrasfoodbook.com/wp-content/uploads/2014/08/vegetables-for-salad.jpg?w=1200&ssl=1'), + ), + ), + ), + + // Discount badge — top left + if (discount > 0) + Positioned( + top: 10, + left: 10, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFF662582), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '$discount% OFF', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ), + ), + + // Favourite button — top right + Positioned( + top: 8, + right: 8, + child: FavButton( + size: 32, + initialValue: _isFav, + onChanged: (val) => setState(() => _isFav = val), + ), + ), + + // Add / Stepper button — bottom right ON image + Positioned( + bottom: 10, + right: 10, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeOutBack, + switchOutCurve: Curves.easeIn, + transitionBuilder: (child, animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: widget.quantity <= 0 + ? GestureDetector( + key: const ValueKey('add_button'), // important! + onTap: widget.onAddTap, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.add_rounded, + color: Color(0xFF662582), + size: 20, + ), + ), + ) + : Container( + key: const ValueKey('counter_row'), // important! + height: 34, + padding: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: widget.onDecrementTap, + child: const Icon( + Icons.remove_rounded, + color: Color(0xFF662582), + size: 18, + ), + ), + const SizedBox(width: 6), + Text( + '${widget.quantity}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 6), + GestureDetector( + onTap: widget.onIncrementTap, + child: const Icon( + Icons.add_rounded, + color: Color(0xFF662582), + size: 18, + ), + ), + ], + ), + ), + ), + ), + + ], + ), + + // ── Info area ── + Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // Category + Text( + item.category, + style: const TextStyle( + fontSize: 10.5, + color: Color(0xFF662582), + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 3), + + // Product name + Text( + item.name, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 6), + + // Rating row + Row( + children: [ + const Icon( + Icons.star_rounded, + size: 13, + color: Color(0xFFFFC107), + ), + const SizedBox(width: 3), + Text( + '${item.rating}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 3), + Text( + '(${item.reviewCount})', + style: const TextStyle( + fontSize: 10.5, + color: Color(0xFF9E9E9E), + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Price row — no button here anymore + Row( + children: [ + Text( + item.price, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 6), + // if (discount > 0) + // Text( + // item.originalPrice, + // style: const TextStyle( + // fontSize: 11, + // color: Color(0xFF9E9E9E), + // decoration: TextDecoration.lineThrough, + // ), + // ), + ], + ), + + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/Store/presentation/controller/store_controller.dart b/lib/features/Store/presentation/controller/store_controller.dart new file mode 100644 index 0000000..804d739 --- /dev/null +++ b/lib/features/Store/presentation/controller/store_controller.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +/// Manages all scroll, cart-bar visibility, back-to-top, and +/// network-retry logic for StoreViewPage. +class StoreViewController extends GetxController { + // ── Scroll ────────────────────────────────────────────── + final ScrollController scrollController = ScrollController(); + + double scrollOffset = 0.0; + bool showBackToTop = false; + bool hideCartBar = false; + + Timer? _cartTimer; + + // ── Mini-cart ──────────────────────────────────────────── + RxBool showMiniCart = false.obs; + + // ── Search ─────────────────────────────────────────────── + final TextEditingController searchController = TextEditingController(); + + // ── Misc ───────────────────────────────────────────────── + var selectedCategoryId = 0.obs; + + @override + void onInit() { + super.onInit(); + scrollController.addListener(_onScroll); + } + + @override + void onClose() { + scrollController.removeListener(_onScroll); + scrollController.dispose(); + searchController.dispose(); + _cartTimer?.cancel(); + super.onClose(); + } + + // ── Scroll handler (used by the ScrollController listener) ── + void _onScroll() { + scrollOffset = scrollController.offset; + showBackToTop = scrollController.offset > 300; + update(['scroll']); + } + + // ── Notification-based handler (NestedScrollView body) ────── + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && + notification.metrics.axis == Axis.vertical) { + final double pixels = notification.metrics.pixels; + final bool isScrollingUp = pixels < scrollOffset; + + scrollOffset = pixels; + + if (isScrollingUp && pixels > 300) { + showBackToTop = true; + } else if (pixels <= 300 || !isScrollingUp) { + showBackToTop = false; + } + + // Hide cart bar immediately on scroll + if (!hideCartBar) { + hideCartBar = true; + showMiniCart.value = false; + update(['cartBar']); + } + + // Re-show cart bar after scroll stops + _cartTimer?.cancel(); + _cartTimer = Timer(const Duration(milliseconds: 250), () { + if (hideCartBar) { + hideCartBar = false; + update(['cartBar']); + } + }); + + update(['scroll']); + } + return false; + } + + // ── Background colour based on scroll position ─────────────── + Color backgroundColorFor(List colors) { + if (colors.isEmpty) return Colors.white; + return colors[(scrollOffset ~/ 300) % colors.length]; + } + + // ── Scroll to top ───────────────────────────────────────────── + void scrollToTop() { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + ); + } + + // ── Network retry ───────────────────────────────────────────── + Future retryNetworkCall(Future Function() onSuccess) async { + final connectivityResult = await Connectivity().checkConnectivity(); + + if (connectivityResult == ConnectivityResult.mobile || + connectivityResult == ConnectivityResult.wifi) { + debugPrint('✅ Internet available, retrying...'); + await onSuccess(); + } else { + debugPrint('❌ Still no internet connection'); + Get.snackbar( + 'No Internet', + 'Please check your connection and try again.', + backgroundColor: Colors.grey[200], + colorText: Colors.black, + snackPosition: SnackPosition.TOP, + ); + } + } +} \ No newline at end of file diff --git a/lib/features/Store/presentation/screens/store_view.dart b/lib/features/Store/presentation/screens/store_view.dart new file mode 100644 index 0000000..3054730 --- /dev/null +++ b/lib/features/Store/presentation/screens/store_view.dart @@ -0,0 +1,454 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart'; + +import '../../../../constants/color_constants.dart'; +import '../../../../constants/font_constants.dart'; +import '../../../../controllers/account_controller/profile.dart'; +import '../../../../controllers/cart_controller/cart.dart'; +import '../../../../controllers/dashboard_controller/dashboard_controller.dart'; +import '../../../../controllers/tenant_list/tenant_controller.dart'; +import '../../../../service/firebase_analytics/analytics_service.dart'; +import '../../../../view/dashboard_view/tenant_profile.dart'; +import '../../../../view/product/tenant_products.dart'; +import '../../../../widgets/text_widget.dart'; +import '../controller/store_controller.dart'; +import '../widgets/store_widgets.dart' hide ZoomIconButton; + + +class StoreViewPage extends StatefulWidget { + const StoreViewPage({super.key}); + + @override + State createState() => _StoreViewPageState(); +} + +class _StoreViewPageState extends State { + // ── Controllers ────────────────────────────────────────────── + final StoreViewController storeCtrl = Get.put(StoreViewController()); + final TenantController tenantCtrl = Get.put(TenantController()); + final CartController cartCtrl = Get.put(CartController()); + + bool status = true; + + // ───────────────────────────────────────────────────────────── + @override + Widget build(BuildContext context) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.white, + statusBarIconBrightness: Brightness.dark, + statusBarBrightness: Brightness.light, + ), + ); + + return SafeArea( + child: Scaffold( + body: GetBuilder( + id: 'scroll', + builder: (ctrl) { + return Stack( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + color: ctrl.backgroundColorFor(ColorConstants.bgColors), + child: NestedScrollViewPlus( + physics: const ClampingScrollPhysics(), + controller: ctrl.scrollController, + headerSliverBuilder: (context, _) => [ + CupertinoSliverRefreshControl( + onRefresh: () async { + await tenantCtrl.loadTenants(); + await Future.delayed(const Duration(milliseconds: 500)); + }, + ), + ], + body: NotificationListener( + onNotification: ctrl.handleScrollNotification, + child: CustomScrollView( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 12)), + + // ── Section header ────────────────────────────── + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: ReusableTextWidget( + text: "Near by Stores", + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: FontConstants.fontFamily, + color: Colors.black, + ), + ), + ), + + // ── Store list ────────────────────────────────── + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 0), + sliver: Obx(() { + // No internet + if (!tenantCtrl.isConnected.value) { + return SliverToBoxAdapter( + child: NoInternetWidget( + onRetry: () => ctrl.retryNetworkCall( + () async => await tenantCtrl.loadTenants(), + ), + ), + ); + } + + // Loading + if (tenantCtrl.isLoading.value) { + return const StoreListShimmer(); + } + + // Empty + if (tenantCtrl.tenants.isEmpty) { + return const SliverToBoxAdapter( + child: NoStoresFound(), + ); + } + + // Store list + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final item = tenantCtrl.tenants[index]; + return ZoomOnTap( + onTap: () => _openStore(item), + child: _StoreCard( + item: item, + status: status, + onInfoTap: () => + Get.to(StoreOverviewScreen()), + onCategoryArrowTap: () => + _openStore(item), + onSubcategoryTap: (_) => + _openStore(item), + ), + ); + }, + childCount: tenantCtrl.tenants.length, + ), + ); + }), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 220)), + SliverToBoxAdapter( + child: Image.asset( + 'assets/images/nearle_copyrights.png'), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ), + ); + } + + // ── Helpers ──────────────────────────────────────────────────── + void _openStore(dynamic item) async { + Get.to(() => ProductsScreen( + tenantId: item.tenantid!, + locationId: item.locationid!, + categoryId: item.categoryid!, + tenantName: item.tenantname!, + locationname: item.locationname!, + tenantLocation: item.suburb!, + tenantImage: item.tenantimage!, + tenantloc: item.locationid!, + subCategoryName: '', + )); + + await AnalyticsService.logEvent( + 'store_viewed', + parameters: { + 'store_id': item.tenantid!, + 'store_name': item.tenantname!, + 'locatiom': item.locationname!, + }, + ); + } +} + +// ───────────────────────────────────────────────────────────────── +// Private store card widget (lives here because it needs item data) +// ───────────────────────────────────────────────────────────────── +class _StoreCard extends StatelessWidget { + final dynamic item; + final bool status; + final VoidCallback onInfoTap; + final VoidCallback onCategoryArrowTap; + final void Function(dynamic subcategory) onSubcategoryTap; + + const _StoreCard({ + required this.item, + required this.status, + required this.onInfoTap, + required this.onCategoryArrowTap, + required this.onSubcategoryTap, + }); + + double _rs(BuildContext context, double size) => + size * (MediaQuery.of(context).size.width / 390); + + double _rh(BuildContext context, double size) => + size * (MediaQuery.of(context).size.height / 844); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 12, top: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE1E5EA), width: 0.55), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.10), + spreadRadius: 0, + blurRadius: 20, + offset: const Offset(0, 6), + ), + BoxShadow( + color: Colors.black.withOpacity(0.06), + spreadRadius: 0, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header row ────────────────────────────────────── + Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: item.tenantname!, + color: Colors.black.withOpacity(0.7), + fontFamily: FontConstants.fontFamily, + fontSize: _rs(context, 23), + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Row( + children: [ + Icon(Icons.location_on, + color: Colors.grey, size: _rs(context, 11)), + ReusableTextWidget( + text: item.locationname!, + color: Colors.black.withOpacity(0.7), + fontFamily: FontConstants.fontFamily, + fontSize: _rs(context, 10), + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ], + ), + ZoomIconButton( + onTap: onInfoTap, + icon: Icons.info_outline, + size: 25, + color: Colors.black87, + ), + ], + ), + ), + + // ── Body: closed or open ───────────────────────────── + status == false + ? const ClosedStoreWidget() + : Column( + children: [ + // Banner + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + height: _rh(context, 150), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: (item.tenantbanner != null && + item.tenantbanner!.isNotEmpty) + ? Image.network( + item.tenantbanner!, + fit: BoxFit.fill, + errorBuilder: (_, __, ___) => Center( + child: Icon( + Icons.image_not_supported_outlined, + size: _rs(context, 40), + color: Colors.white), + ), + ) + : Image.network( + 'https://img.freepik.com/free-psd/healthy-eating-lifestyle-banner-template_23-2149087275.jpg', + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ), + + // Category row + Padding( + padding: const EdgeInsets.only(left: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ReusableTextWidget( + text: 'Category', + color: Colors.black.withOpacity(0.7), + fontFamily: FontConstants.fontFamily, + fontSize: _rs(context, 15), + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + IconButton( + onPressed: onCategoryArrowTap, + icon: Icon( + Icons.arrow_circle_right_outlined, + color: Colors.black.withOpacity(0.6), + size: _rs(context, 24), + ), + ), + ], + ), + ), + + // Subcategories + SizedBox( + height: _rh(context, 160), + child: (item.subcategories != null && + item.subcategories!.isNotEmpty) + ? ListView.builder( + scrollDirection: Axis.horizontal, + padding: + const EdgeInsets.symmetric(horizontal: 12), + itemCount: item.subcategories!.length, + itemBuilder: (context, index) { + final product = item.subcategories![index]; + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => onSubcategoryTap(product), + child: Container( + margin: const EdgeInsets.only( + bottom: 14, right: 12, top: 2), + height: _rh(context, 180), + width: _rs(context, 120), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.black12, width: 0.20), + boxShadow: [ + BoxShadow( + color: + Colors.black.withOpacity(0.10), + spreadRadius: 0, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: _rh(context, 12)), + ClipOval( + child: Image.network( + product.image ?? '', + width: _rs(context, 80), + height: _rs(context, 80), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + Icon( + Icons.image_not_supported, + color: Colors.grey, + size: _rs(context, 30), + ), + loadingBuilder: + (_, child, progress) { + if (progress == null) + return child; + return const Center( + child: + CircularProgressIndicator( + color: Colors.grey), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8), + child: Center( + child: ReusableTextWidget( + text: product.subcatname!, + color: Colors.black + .withOpacity(0.7), + fontFamily: + FontConstants.fontFamily, + fontSize: _rs(context, 12), + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + SizedBox(height: _rh(context, 12)), + ], + ), + ), + ); + }, + ) + : const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'No products available', + style: TextStyle(color: Colors.grey), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/Store/presentation/widgets/store_widgets.dart b/lib/features/Store/presentation/widgets/store_widgets.dart new file mode 100644 index 0000000..b4c3a2e --- /dev/null +++ b/lib/features/Store/presentation/widgets/store_widgets.dart @@ -0,0 +1,425 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../../../../constants/color_constants.dart'; +import '../../../../constants/font_constants.dart'; +import '../../../../widgets/text_widget.dart'; + +// ───────────────────────────────────────────── +// Zoom-on-tap wrapper +// ───────────────────────────────────────────── +class ZoomOnTap extends StatefulWidget { + final Widget child; + final VoidCallback onTap; + + const ZoomOnTap({ + super.key, + required this.child, + required this.onTap, + }); + + @override + State createState() => _ZoomOnTapState(); +} + +class _ZoomOnTapState extends State { + double _scale = 1.0; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => _scale = 0.96), + onTapUp: (_) { + setState(() => _scale = 1.0); + widget.onTap(); + }, + onTapCancel: () => setState(() => _scale = 1.0), + child: AnimatedScale( + scale: _scale, + duration: const Duration(milliseconds: 120), + curve: Curves.easeOut, + child: widget.child, + ), + ); + } +} + +// ───────────────────────────────────────────── +// Zoom icon button +// ───────────────────────────────────────────── +class ZoomIconButton extends StatefulWidget { + final VoidCallback onTap; + final IconData icon; + final double size; + final Color color; + + const ZoomIconButton({ + super.key, + required this.onTap, + required this.icon, + this.size = 24, + this.color = Colors.black87, + }); + + @override + State createState() => _ZoomIconButtonState(); +} + +class _ZoomIconButtonState extends State { + double _scale = 1.0; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => _scale = 0.85), + onTapUp: (_) { + setState(() => _scale = 1.0); + widget.onTap(); + }, + onTapCancel: () => setState(() => _scale = 1.0), + child: AnimatedScale( + scale: _scale, + duration: const Duration(milliseconds: 120), + curve: Curves.easeOut, + child: Icon(widget.icon, size: widget.size, color: widget.color), + ), + ); + } +} + +// ───────────────────────────────────────────── +// Search bar +// ───────────────────────────────────────────── +class StoreSearchBar extends StatelessWidget { + final TextEditingController controller; + final String hint; + final VoidCallback? onTap; + + const StoreSearchBar({ + super.key, + required this.controller, + this.hint = 'Search', + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Icon(Icons.search, color: Colors.grey.shade600), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: controller, + onTap: onTap, + decoration: InputDecoration( + hintText: hint, + border: InputBorder.none, + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + ), + ), + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────────── +// No internet widget +// ───────────────────────────────────────────── +class NoInternetWidget extends StatelessWidget { + final VoidCallback onRetry; + + const NoInternetWidget({super.key, required this.onRetry}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Lottie.asset( + 'assets/lotties/no_internet.json', + width: 200, + height: 200, + fit: BoxFit.contain, + ), + const SizedBox(height: 16), + ReusableTextWidget( + text: 'No Internet Connection', + color: Colors.grey[700]!, + fontFamily: FontConstants.fontFamily, + fontSize: 18, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: onRetry, + style: ElevatedButton.styleFrom( + backgroundColor: ColorConstants.primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Retry', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────────── +// No stores found +// ───────────────────────────────────────────── +class NoStoresFound extends StatelessWidget { + const NoStoresFound({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Lottie.asset('assets/lotties/QR Code scan on phone.json'), + const SizedBox(height: 10), + ReusableTextWidget( + text: 'We don\'t deliver to this location yet. Try browsing nearby stores instead.', + textAlign: TextAlign.center, + maxLines: 2, + fontSize: 14, + ), + ], + ); + } +} +// ───────────────────────────────────────────── +// Closed store UI +// ───────────────────────────────────────────── +class ClosedStoreWidget extends StatelessWidget { + const ClosedStoreWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Lottie.asset( + 'assets/lotties/shop.json', + height: 140, + repeat: true, + animate: true, + fit: BoxFit.contain, + ), + ), + const SizedBox(height: 16), + Text( + 'Store is currently closed', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.7), + ), + ), + const SizedBox(height: 6), + Text( + 'Please come back later', + style: TextStyle( + fontSize: 14, + color: Colors.black.withOpacity(0.7), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + } +} + +// ───────────────────────────────────────────── +// List shimmer (loading placeholder) +// ───────────────────────────────────────────── +class StoreListShimmer extends StatelessWidget { + const StoreListShimmer({super.key}); + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 160, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 12), + Container( + height: 16, + width: MediaQuery.of(context).size.width * 0.6, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 8), + Container( + height: 14, + width: MediaQuery.of(context).size.width * 0.4, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Container( + height: 36, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Container( + height: 36, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + childCount: 5, + ), + ); + } +} + +// ───────────────────────────────────────────── +// Grid shimmer (subcategory loading placeholder) +// ───────────────────────────────────────────── +class SubcategoryGridShimmer extends StatelessWidget { + const SubcategoryGridShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + double maxCrossAxisExtent = 250; + if (screenWidth > 1200) { + maxCrossAxisExtent = 300; + } else if (screenWidth > 800) { + maxCrossAxisExtent = 280; + } + + return SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, left: 10, right: 10), + child: Container( + height: 140, + width: 140, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Container(height: 20, width: 100, color: Colors.white), + ), + Container( + height: 35, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)), + ), + ), + ], + ), + ), + ); + }, + childCount: 6, + ), + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + mainAxisExtent: 230, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/category/prestation/screens/category.dart b/lib/features/category/prestation/screens/category.dart new file mode 100644 index 0000000..1f8ab78 --- /dev/null +++ b/lib/features/category/prestation/screens/category.dart @@ -0,0 +1,1934 @@ +import 'package:flutter/material.dart'; + + + +const kGreen = Color(0xFF1E7A34); +const kGreenBg = Color(0xFFEFF7EF); + +/// Centers content and caps its width on large / web / tablet screens so +/// the phone-style UI never stretches ugly-wide, while still filling +/// narrow phone screens edge to edge. +class ResponsiveBody extends StatelessWidget { + final Widget child; + final double maxWidth; + const ResponsiveBody({super.key, required this.child, this.maxWidth = 520}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: child, + ), + ); + } +} + +// ---------------------------------------------------------------------- +// MODELS +// ---------------------------------------------------------------------- + +class ServiceCategory { + final String label; + final String imageUrl; + const ServiceCategory(this.label, this.imageUrl); +} + +class ServiceItem { + final String title; + final String desc; + final String time; + final String price; + final String imageUrl; + final Color bgColor; + + const ServiceItem({ + required this.title, + required this.desc, + required this.time, + required this.price, + required this.imageUrl, + required this.bgColor, + }); + + int get basePrice { + final digits = RegExp(r'\d+').firstMatch(price)?.group(0); + return digits != null ? int.parse(digits) : 299; + } + + String get providerName { + const map = { + 'Plumbing': 'Ramesh Kumar', + 'Electrical': 'Suresh Iyer', + 'Cleaning': 'Anita Sharma', + 'Appliance Repair': 'Vikram Singh', + 'Carpentry': 'Manoj Verma', + 'Painting': 'Arjun Nair', + 'Pest Control': 'Deepak Rao', + 'Masonry': 'Karthik Reddy', + }; + return map[title] ?? 'Verified Professional'; + } + + List get included => const [ + 'Inspection & diagnosis', + 'Standard service & repair', + 'Minor parts & fittings', + 'Workmanship warranty (7 days)', + ]; +} + +enum BookingStatus { booked, confirmed, inProgress, completed } + +class BookingInfo { + final ServiceItem service; + final String bookingId; + final String address; + final DateTime bookedOn; + final DateTime confirmedOn; + final DateTime visitDate; + final String visitTimeSlot; + final int visitCharge; + final BookingStatus status; + final double rating; + final int jobsDone; + + BookingInfo({ + required this.service, + required this.bookingId, + required this.address, + required this.bookedOn, + required this.confirmedOn, + required this.visitDate, + required this.visitTimeSlot, + this.visitCharge = 99, + this.status = BookingStatus.inProgress, + this.rating = 4.8, + this.jobsDone = 230, + }); + + int get taxes => ((service.basePrice + visitCharge) * 0.08).round(); + int get total => service.basePrice + visitCharge + taxes; + + factory BookingInfo.fromService(ServiceItem s) { + final now = DateTime.now(); + return BookingInfo( + service: s, + bookingId: 'BK${(s.title.hashCode.toUnsigned(20)).toString().padLeft(9, '0').substring(0, 9)}', + address: 'Koramangala, 4th Block, Bangalore 560034', + bookedOn: now.subtract(const Duration(days: 2)), + confirmedOn: now.subtract(const Duration(days: 2, hours: -1)), + visitDate: now.add(const Duration(days: 1)), + visitTimeSlot: '10:00 AM', + ); + } +} + +// ---------------------------------------------------------------------- +// DATE / TIME HELPERS (no intl dependency needed) +// ---------------------------------------------------------------------- + +const _months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' +]; + +String formatDate(DateTime d) => '${d.day} ${_months[d.month - 1]} ${d.year}'; + +String formatTime(DateTime d) { + final hour = d.hour % 12 == 0 ? 12 : d.hour % 12; + final minute = d.minute.toString().padLeft(2, '0'); + final period = d.hour >= 12 ? 'PM' : 'AM'; + return '$hour:$minute $period'; +} + +// ---------------------------------------------------------------------- +// HOME PAGE +// ---------------------------------------------------------------------- + +class LocalServicesHome extends StatelessWidget { + const LocalServicesHome({super.key}); + + static const categories = [ + ServiceCategory('Plumbing', 'https://img.icons8.com/color/96/water-tap.png'), + ServiceCategory('Electrical', 'https://img.icons8.com/color/96/lightning-bolt.png'), + ServiceCategory('Cleaning', 'https://img.icons8.com/color/96/cleaning-bucket.png'), + ServiceCategory('Appliances', 'https://img.icons8.com/color/96/washing-machine.png'), + ServiceCategory('Carpentry', 'https://img.icons8.com/color/96/wood.png'), + ]; + + static const services = [ + ServiceItem( + title: 'Plumbing', + desc: 'Leak repairs, installations, pipe fitting & more', + time: '60-90 mins', + price: 'From ₹299', + imageUrl: 'https://img.icons8.com/color/96/water-tap.png', + bgColor: Color(0xFFE7EEF7), + ), + ServiceItem( + title: 'Electrical', + desc: 'Wiring, light fitting, fan installation & more', + time: '45-90 mins', + price: 'From ₹249', + imageUrl: 'https://img.icons8.com/color/96/lightning-bolt.png', + bgColor: Color(0xFFFCEFD9), + ), + ServiceItem( + title: 'Cleaning', + desc: 'Home, bathroom, kitchen cleaning & more', + time: '60-120 mins', + price: 'From ₹349', + imageUrl: 'https://img.icons8.com/color/96/cleaning-bucket.png', + bgColor: Color(0xFFE7EEF7), + ), + ServiceItem( + title: 'Appliance Repair', + desc: 'AC, fridge, washing machine, microwave & more', + time: '60-120 mins', + price: 'From ₹399', + imageUrl: 'https://img.icons8.com/color/96/washing-machine.png', + bgColor: Color(0xFFEDEDED), + ), + ServiceItem( + title: 'Carpentry', + desc: 'Furniture repair, installation, wood work & more', + time: '60-120 mins', + price: 'From ₹349', + imageUrl: 'https://img.icons8.com/color/96/wood.png', + bgColor: Color(0xFFFBE5E1), + ), + ServiceItem( + title: 'Painting', + desc: 'Wall painting, touch-ups, distemper & more', + time: '120-240 mins', + price: 'From ₹599', + imageUrl: 'https://img.icons8.com/color/96/paint-roller.png', + bgColor: Color(0xFFE7F4E9), + ), + ServiceItem( + title: 'Pest Control', + desc: 'Cockroach, termite, rodent control & more', + time: '45-90 mins', + price: 'From ₹299', + imageUrl: 'https://img.icons8.com/color/96/spray.png', + bgColor: Color(0xFFFBE5E1), + ), + ServiceItem( + title: 'Masonry', + desc: 'Wall repair, plastering, tiles, waterproofing & more', + time: '120-240 mins', + price: 'From ₹599', + imageUrl: 'https://img.icons8.com/color/96/shovel.png', + bgColor: Color(0xFFEDEDED), + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: ResponsiveBody( + child: ListView( + padding: const EdgeInsets.only(bottom: 24), + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 16, 8), + child: Row( + children: [ + InkWell( + onTap: () => Navigator.of(context).maybePop(), + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.arrow_back, size: 22), + ), + ), + const SizedBox(width: 4), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Local Services', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + letterSpacing: -0.3, + height: 1.2, + ), + ), + Text( + 'Trusted experts at your doorstep', + style: TextStyle(fontSize: 12, color: Colors.black54), + ), + ], + ), + ), + InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.location_on_outlined, size: 22), + ), + ), + ], + ), + ), + + // Search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + const SizedBox(width: 14), + Icon(Icons.search, color: Colors.grey.shade500, size: 21), + const SizedBox(width: 8), + Expanded( + child: TextField( + style: const TextStyle(fontSize: 13.5, color: Colors.black87), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Search for services (Plumbing, Electrician...)', + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 13), + ), + ), + ), + Container(width: 1, height: 22, color: Colors.grey.shade200), + InkWell( + onTap: () {}, + customBorder: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(13), + child: Icon(Icons.mic_none_rounded, color: Colors.grey.shade600, size: 20), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Category icons row + SizedBox( + height: 92, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12), + children: [ + const _CategoryCircle( + label: 'All Services', + selected: true, + child: Icon(Icons.grid_view, color: kGreen), + ), + for (final c in categories) + _CategoryCircle(label: c.label, imageUrl: c.imageUrl), + const _CategoryCircle( + label: 'More', + child: Icon(Icons.keyboard_arrow_down), + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Trust badges + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + decoration: BoxDecoration( + color: kGreenBg, + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _TrustBadge( + icon: Icons.verified_user_outlined, + title: 'Verified Professionals', + subtitle: 'Background checked', + ), + _TrustBadge( + icon: Icons.access_time, + title: 'On-time Service', + subtitle: 'Punctual & reliable', + ), + _TrustBadge( + icon: Icons.lock_outline, + title: 'Safe Payments', + subtitle: '100% secured', + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // Popular Services header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Popular Services', style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold)), + Row( + children: [ + Text('View All', style: TextStyle(color: kGreen, fontWeight: FontWeight.w600, fontSize: 13)), + const Icon(Icons.chevron_right, color: kGreen, size: 18), + ], + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Services grid — FIXED: uses mainAxisExtent (a real pixel + // height) instead of a childAspectRatio, so the card can + // never overflow no matter the text length, font-scale + // setting, or screen width. Column count also adapts for + // wider / tablet screens. + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final crossAxisCount = width > 700 + ? 4 + : width > 460 + ? 3 + : 2; + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: services.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + mainAxisExtent: 196, + ), + itemBuilder: (context, index) => _ServiceCard(service: services[index]), + ); + }, + ), + ), + + const SizedBox(height: 20), + + // Banner + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: kGreenBg, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Save More with Service Plan', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), + const SizedBox(height: 4), + const Text( + 'Priority support • Regular maintenance • Exclusive discounts', + style: TextStyle(fontSize: 12, color: Colors.black54), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), + ), + child: const Text('Explore Plans'), + ), + ], + ), + ), + const SizedBox(width: 8), + Image.network( + 'https://img.icons8.com/color/96/maintenance.png', + width: 70, + height: 70, + errorBuilder: (c, e, s) => const Icon(Icons.handyman, size: 60), + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Why book with us + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text('Why book with us?', style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final crossAxisCount = width > 700 ? 4 : 2; + return GridView.count( + crossAxisCount: crossAxisCount, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1.6, + children: const [ + _WhyBookItem( + icon: Icons.verified_user_outlined, + iconBg: Color(0xFFE2F3E4), + iconColor: kGreen, + title: 'Verified & Rated\nProfessionals', + ), + _WhyBookItem( + icon: Icons.calendar_today_outlined, + iconBg: Color(0xFFE5ECFB), + iconColor: Color(0xFF2255CC), + title: 'Easy Booking &\nReschedule', + ), + _WhyBookItem( + icon: Icons.call_outlined, + iconBg: Color(0xFFE2F3E4), + iconColor: kGreen, + title: 'Real-time Updates\n& Tracking', + ), + _WhyBookItem( + icon: Icons.account_balance_wallet_outlined, + iconBg: Color(0xFFDDF3F1), + iconColor: Color(0xFF128C7E), + title: 'Cashless Payments\n& Invoices', + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} + +class _CategoryCircle extends StatelessWidget { + final String label; + final String? imageUrl; + final Widget? child; + final bool selected; + + const _CategoryCircle({ + required this.label, + this.imageUrl, + this.child, + this.selected = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selected ? kGreenBg : Colors.white, + border: Border.all( + color: selected ? kGreen : Colors.grey.shade300, + width: selected ? 1.5 : 1, + ), + ), + child: Center( + child: child ?? + (imageUrl != null + ? ClipOval( + child: Image.network( + imageUrl!, + width: 30, + height: 30, + fit: BoxFit.cover, + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 1.5), + ); + }, + errorBuilder: (c, e, s) => const Icon(Icons.build, size: 24), + ), + ) + : const SizedBox()), + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 64, + child: Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: selected ? FontWeight.w600 : FontWeight.normal, + color: selected ? kGreen : Colors.black87, + ), + ), + ), + ], + ), + ); + } +} + +class _TrustBadge extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + + const _TrustBadge({required this.icon, required this.title, required this.subtitle}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: kGreen, size: 22), + const SizedBox(height: 6), + Text(title, textAlign: TextAlign.center, maxLines: 2, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600)), + const SizedBox(height: 2), + Text(subtitle, textAlign: TextAlign.center, maxLines: 1, style: const TextStyle(fontSize: 10, color: Colors.black54)), + ], + ), + ); + } +} + +class _ServiceCard extends StatelessWidget { + final ServiceItem service; + const _ServiceCard({required this.service}); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(18), + child: InkWell( + borderRadius: BorderRadius.circular(18), + splashColor: kGreen.withOpacity(0.08), + highlightColor: kGreen.withOpacity(0.04), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ServiceDetailPage(service: service)), + ); + }, + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.black.withOpacity(0.04)), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 16, offset: const Offset(0, 6)), + BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 2, offset: const Offset(0, 1)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [service.bgColor, service.bgColor.withOpacity(0.6)], + ), + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + service.imageUrl, + width: 26, + height: 26, + fit: BoxFit.contain, + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 1.5), + ); + }, + errorBuilder: (c, e, s) => const Icon(Icons.build, size: 22, color: Colors.white), + ), + ), + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration(color: Colors.black.withOpacity(0.04), shape: BoxShape.circle), + child: const Icon(Icons.arrow_outward_rounded, size: 14, color: Colors.black54), + ), + ], + ), + const SizedBox(height: 12), + Text( + service.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.5, + height: 1.2, + letterSpacing: -0.2, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 4), + Expanded( + child: Text( + service.desc, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 11.5, height: 1.35, color: Colors.black.withOpacity(0.5)), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.access_time_rounded, size: 12, color: Colors.black.withOpacity(0.4)), + const SizedBox(width: 3), + Flexible( + child: Text( + service.time, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 10.5, color: Colors.black.withOpacity(0.45)), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: kGreen.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), + child: Text( + service.price, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w800, color: kGreen), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _WhyBookItem extends StatelessWidget { + final IconData icon; + final Color iconBg; + final Color iconColor; + final String title; + + const _WhyBookItem({required this.icon, required this.iconBg, required this.iconColor, required this.title}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade200), borderRadius: BorderRadius.circular(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration(color: iconBg, shape: BoxShape.circle), + child: Icon(icon, color: iconColor, size: 18), + ), + const SizedBox(height: 8), + Text(title, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} + +// ---------------------------------------------------------------------- +// SERVICE DETAIL PAGE (matches screenshot 1 of the second image) +// ---------------------------------------------------------------------- + +class ServiceDetailPage extends StatefulWidget { + final ServiceItem service; + const ServiceDetailPage({super.key, required this.service}); + + @override + State createState() => _ServiceDetailPageState(); +} + +class _ServiceDetailPageState extends State { + late BookingInfo booking; + + @override + void initState() { + super.initState(); + booking = BookingInfo.fromService(widget.service); + } + + void _openReschedulePauseSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ReschedulePauseSheet( + booking: booking, + onRescheduled: (newDate, newSlot) { + setState(() { + booking = BookingInfo( + service: booking.service, + bookingId: booking.bookingId, + address: booking.address, + bookedOn: booking.bookedOn, + confirmedOn: booking.confirmedOn, + visitDate: newDate, + visitTimeSlot: newSlot, + status: booking.status, + rating: booking.rating, + jobsDone: booking.jobsDone, + ); + }); + }, + ), + ); + } + + void _confirmCancel() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Cancel Booking?'), + content: const Text('Are you sure you want to cancel this booking? This action cannot be undone.'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Keep Booking')), + TextButton( + onPressed: () { + Navigator.pop(ctx); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Booking cancelled')), + ); + }, + child: const Text('Cancel Booking', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final s = widget.service; + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: ResponsiveBody( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + children: [ + // top bar + Row( + children: [ + InkWell( + onTap: () => Navigator.of(context).maybePop(), + borderRadius: BorderRadius.circular(20), + child: const Padding(padding: EdgeInsets.all(8), child: Icon(Icons.arrow_back, size: 22)), + ), + const Spacer(), + InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(20), + child: const Padding(padding: EdgeInsets.all(8), child: Icon(Icons.more_vert, size: 22)), + ), + ], + ), + const SizedBox(height: 4), + + // header: icon + title + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration(color: s.bgColor, borderRadius: BorderRadius.circular(16)), + padding: const EdgeInsets.all(14), + child: Image.network( + s.imageUrl, + fit: BoxFit.contain, + loadingBuilder: (c, child, progress) => progress == null + ? child + : const Center(child: CircularProgressIndicator(strokeWidth: 1.5)), + errorBuilder: (c, e, s2) => const Icon(Icons.build), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${s.title} Service', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800), maxLines: 2), + const SizedBox(height: 3), + const Text('Trusted experts at your service', + style: TextStyle(fontSize: 12.5, color: Colors.black54)), + const SizedBox(height: 8), + InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => BookingTimelinePage(booking: booking)), + ), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: kGreenBg, borderRadius: BorderRadius.circular(20)), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Active Booking', + style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w700, color: kGreen)), + SizedBox(width: 3), + Icon(Icons.chevron_right, size: 14, color: kGreen), + ], + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Next visit card + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: kGreenBg, borderRadius: BorderRadius.circular(14)), + child: Row( + children: [ + const Icon(Icons.calendar_today_outlined, color: kGreen, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Next Visit', style: TextStyle(fontSize: 11.5, color: Colors.black54)), + const SizedBox(height: 2), + Text('Tomorrow • ${booking.visitTimeSlot}', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700)), + ], + ), + ), + OutlinedButton( + onPressed: _openReschedulePauseSheet, + style: OutlinedButton.styleFrom( + foregroundColor: kGreen, + side: const BorderSide(color: kGreen), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + ), + child: const Text('Reschedule', style: TextStyle(fontSize: 12.5)), + ), + ], + ), + ), + const SizedBox(height: 18), + + // Booking Details card + _SectionCard( + title: 'Booking Details', + trailing: TextButton(onPressed: () {}, child: const Text('Edit')), + child: Column( + children: [ + _DetailRow(icon: Icons.grid_view_rounded, label: 'Service', value: '${s.title} Service'), + _DetailRow(icon: Icons.location_on_outlined, label: 'Address', value: booking.address), + _DetailRow(icon: Icons.calendar_today_outlined, label: 'Date & Time', + value: 'Tomorrow, ${formatDate(booking.visitDate)} • ${booking.visitTimeSlot}'), + _DetailRow(icon: Icons.access_time, label: 'Duration', value: 'Estimated ${s.time}'), + const Divider(height: 24), + Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: kGreen.withOpacity(0.12), + child: Text( + s.providerName.split(' ').map((w) => w[0]).take(2).join(), + style: const TextStyle(color: kGreen, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(s.providerName, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14)), + const SizedBox(height: 2), + Row( + children: [ + const Icon(Icons.star, size: 14, color: Colors.amber), + const SizedBox(width: 3), + Text('${booking.rating} (${booking.jobsDone} jobs)', + style: const TextStyle(fontSize: 12, color: Colors.black54)), + ], + ), + ], + ), + ), + InkWell( + onTap: () { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Calling ${s.providerName}...'))); + }, + customBorder: const CircleBorder(), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(border: Border.all(color: kGreen), shape: BoxShape.circle), + child: const Icon(Icons.call, color: kGreen, size: 18), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // Price details + _SectionCard( + title: 'Price Details', + child: Column( + children: [ + _PriceRow('Service Charges', s.basePrice), + _PriceRow('Visit Charges', booking.visitCharge), + _PriceRow('Taxes & Fees', booking.taxes), + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Total Payable', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + Text('₹${booking.total}', style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.check_circle, color: kGreen, size: 15), + const SizedBox(width: 6), + Expanded( + child: Text('Payment will be collected after service completion', + style: TextStyle(fontSize: 11.5, color: Colors.black.withOpacity(0.55))), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // What's included + _SectionCard( + title: "What's Included", + child: Column( + children: [ + for (final item in s.included) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + const Icon(Icons.check_circle_outline, color: kGreen, size: 18), + const SizedBox(width: 10), + Expanded(child: Text(item, style: const TextStyle(fontSize: 13))), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Manage booking + _SectionCard( + title: 'Manage Booking', + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final crossAxisCount = width > 420 ? 4 : 2; + return GridView.count( + crossAxisCount: crossAxisCount, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1.5, + children: [ + _ManageAction( + icon: Icons.calendar_today_outlined, + label: 'Reschedule', + color: kGreen, + bg: kGreenBg, + onTap: _openReschedulePauseSheet, + ), + _ManageAction( + icon: Icons.pause_circle_outline, + label: 'Pause / Hold', + color: const Color(0xFF2255CC), + bg: const Color(0xFFE5ECFB), + onTap: _openReschedulePauseSheet, + ), + _ManageAction( + icon: Icons.skip_next_outlined, + label: 'Skip This Visit', + color: const Color(0xFFB07A15), + bg: const Color(0xFFFCEFD9), + onTap: () => ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Visit skipped'))), + ), + _ManageAction( + icon: Icons.cancel_outlined, + label: 'Cancel Booking', + color: Colors.red.shade700, + bg: const Color(0xFFFBE5E1), + onTap: _confirmCancel, + ), + ], + ); + }, + ), + ), + const SizedBox(height: 16), + + // Need help + _SectionCard( + title: 'Need Help?', + child: InkWell( + onTap: () => ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Opening support chat...'))), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: kGreenBg, shape: BoxShape.circle), + child: const Icon(Icons.headset_mic_outlined, color: kGreen, size: 20), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Chat with Support', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13.5)), + SizedBox(height: 2), + Text("We're here to help you 24x7", + style: TextStyle(fontSize: 11.5, color: Colors.black54)), + ], + ), + ), + const Icon(Icons.chevron_right, color: Colors.black45), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Save more banner + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: kGreenBg, borderRadius: BorderRadius.circular(14)), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Save More with Service Plan', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), + const SizedBox(height: 4), + Text('Get regular ${s.title.toLowerCase()} check-ups & priority support.', + style: const TextStyle(fontSize: 11.5, color: Colors.black54)), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), + ), + child: const Text('Explore Plans', style: TextStyle(fontSize: 12.5)), + ), + ], + ), + ), + const SizedBox(width: 8), + Image.network( + 'https://img.icons8.com/color/96/maintenance.png', + width: 56, + height: 56, + errorBuilder: (c, e, st) => const Icon(Icons.handyman, size: 48), + ), + ], + ), + ), + const SizedBox(height: 16), + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.shield_outlined, size: 14, color: Colors.black.withOpacity(0.4)), + const SizedBox(width: 6), + Text('Your data and payments are 100% secure', + style: TextStyle(fontSize: 11, color: Colors.black.withOpacity(0.45))), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _SectionCard extends StatelessWidget { + final String title; + final Widget child; + final Widget? trailing; + const _SectionCard({required this.title, required this.child, this.trailing}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + if (trailing != null) trailing!, + ], + ), + const SizedBox(height: 12), + child, + ], + ), + ); + } +} + +class _DetailRow extends StatelessWidget { + final IconData icon; + final String label; + final String value; + const _DetailRow({required this.icon, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 17, color: Colors.black45), + const SizedBox(width: 10), + SizedBox( + width: 84, + child: Text(label, style: const TextStyle(fontSize: 12.5, color: Colors.black54)), + ), + Expanded( + child: Text(value, style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w600)), + ), + ], + ), + ); + } +} + +class _PriceRow extends StatelessWidget { + final String label; + final int amount; + const _PriceRow(this.label, this.amount); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 13, color: Colors.black54)), + Text('₹$amount', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} + +class _ManageAction extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + final Color bg; + final VoidCallback onTap; + const _ManageAction({required this.icon, required this.label, required this.color, required this.bg, required this.onTap}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(height: 6), + Text( + label, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 10.5, fontWeight: FontWeight.w700, color: color), + ), + ], + ), + ), + ); + } +} + +// ---------------------------------------------------------------------- +// BOOKING TIMELINE PAGE (matches screenshot 2 of the second image) +// ---------------------------------------------------------------------- + +class BookingTimelinePage extends StatelessWidget { + final BookingInfo booking; + const BookingTimelinePage({super.key, required this.booking}); + + @override + Widget build(BuildContext context) { + final s = booking.service; + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + title: const Text('Booking Details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)), + centerTitle: false, + ), + body: SafeArea( + child: ResponsiveBody( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + children: [ + // header card + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + width: 52, + height: 52, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: s.bgColor, borderRadius: BorderRadius.circular(12)), + child: Image.network( + s.imageUrl, + fit: BoxFit.contain, + errorBuilder: (c, e, st) => const Icon(Icons.build), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${s.title} Service', style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration(color: kGreenBg, borderRadius: BorderRadius.circular(20)), + child: const Text('Active Booking', + style: TextStyle(fontSize: 10.5, fontWeight: FontWeight.w700, color: kGreen)), + ), + const SizedBox(height: 4), + Text('Booking ID: ${booking.bookingId}', + style: const TextStyle(fontSize: 11, color: Colors.black45)), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + // horizontal stepper + _Stepper(status: booking.status, bookedOn: booking.bookedOn, confirmedOn: booking.confirmedOn, visitDate: booking.visitDate), + const SizedBox(height: 20), + + _SectionCard( + title: 'Visit Timeline', + child: _VisitTimeline(booking: booking), + ), + const SizedBox(height: 16), + + _SectionCard( + title: 'Service Address', + child: Row( + children: [ + const Icon(Icons.home_outlined, color: kGreen, size: 20), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Home', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13.5)), + const SizedBox(height: 2), + Text(booking.address, style: const TextStyle(fontSize: 12, color: Colors.black54)), + ], + ), + ), + TextButton(onPressed: () {}, child: const Text('View Map')), + ], + ), + ), + const SizedBox(height: 16), + + _SectionCard( + title: 'Payment Summary', + child: Column( + children: [ + _PriceRow('Service Charges', s.basePrice), + _PriceRow('Visit Charges', booking.visitCharge), + _PriceRow('Taxes & Fees', booking.taxes), + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Total Payable', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + Text('₹${booking.total}', style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15)), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + const Icon(Icons.check_circle, color: kGreen, size: 14), + const SizedBox(width: 6), + Expanded( + child: Text('Payment will be collected after service completion', + style: TextStyle(fontSize: 11, color: Colors.black.withOpacity(0.55))), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () => ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Calling ${s.providerName}...'))), + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text('Contact ${s.providerName.split(' ').first}', + style: const TextStyle(fontWeight: FontWeight.w700)), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _Stepper extends StatelessWidget { + final BookingStatus status; + final DateTime bookedOn; + final DateTime confirmedOn; + final DateTime visitDate; + const _Stepper({required this.status, required this.bookedOn, required this.confirmedOn, required this.visitDate}); + + @override + Widget build(BuildContext context) { + final steps = [ + ('Booked', formatDate(bookedOn), true), + ('Confirmed', formatDate(confirmedOn), true), + ('In Progress', formatDate(visitDate), status.index >= BookingStatus.inProgress.index), + ('Completed', 'Pending', status.index >= BookingStatus.completed.index), + ]; + return Row( + children: [ + for (int i = 0; i < steps.length; i++) ...[ + Expanded( + child: Column( + children: [ + Row( + children: [ + if (i > 0) + Expanded( + child: Container( + height: 2, + color: steps[i].$3 ? kGreen : Colors.grey.shade300, + ), + ) + else + const Spacer(), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: steps[i].$3 ? kGreen : Colors.white, + border: Border.all(color: steps[i].$3 ? kGreen : Colors.grey.shade300, width: 2), + ), + child: steps[i].$3 + ? const Icon(Icons.check, size: 13, color: Colors.white) + : null, + ), + if (i < steps.length - 1) + Expanded( + child: Container( + height: 2, + color: steps[i + 1].$3 ? kGreen : Colors.grey.shade300, + ), + ) + else + const Spacer(), + ], + ), + const SizedBox(height: 6), + Text(steps[i].$1, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 10.5, fontWeight: FontWeight.w700, color: steps[i].$3 ? Colors.black87 : Colors.black38)), + Text(steps[i].$2, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 9.5, color: Colors.black45)), + ], + ), + ), + ], + ], + ); + } +} + +class _VisitTimeline extends StatelessWidget { + final BookingInfo booking; + const _VisitTimeline({required this.booking}); + + @override + Widget build(BuildContext context) { + final items = [ + ('Booking Confirmed', '${formatDate(booking.confirmedOn)} • ${formatTime(booking.confirmedOn)}', true), + ('Provider Assigned', '${formatDate(booking.confirmedOn)} • ${formatTime(booking.confirmedOn.add(const Duration(hours: 1)))}', true), + ('On the Way', '${formatDate(booking.visitDate)} • ${formatTime(booking.visitDate.subtract(const Duration(minutes: 30)))}', true), + ('In Progress', '${formatDate(booking.visitDate)} • ${booking.visitTimeSlot}', booking.status.index >= BookingStatus.inProgress.index), + ('Completed', 'Pending', booking.status.index >= BookingStatus.completed.index), + ]; + return Column( + children: [ + for (int i = 0; i < items.length; i++) + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: items[i].$3 ? kGreen : Colors.grey.shade300, + ), + ), + if (i != items.length - 1) + Expanded( + child: Container(width: 2, color: Colors.grey.shade200), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(items[i].$1, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: items[i].$3 ? Colors.black87 : Colors.black38)), + const SizedBox(height: 2), + Text(items[i].$2, style: const TextStyle(fontSize: 11.5, color: Colors.black45)), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } +} + +// ---------------------------------------------------------------------- +// RESCHEDULE / PAUSE BOTTOM SHEET (matches screenshot 3 of second image) +// ---------------------------------------------------------------------- + +enum _RescheduleOption { reschedule, pause, skip } + +class ReschedulePauseSheet extends StatefulWidget { + final BookingInfo booking; + final void Function(DateTime newDate, String newSlot)? onRescheduled; + const ReschedulePauseSheet({super.key, required this.booking, this.onRescheduled}); + + @override + State createState() => _ReschedulePauseSheetState(); +} + +class _ReschedulePauseSheetState extends State { + _RescheduleOption selected = _RescheduleOption.reschedule; + late DateTime newDate; + String newSlot = '11:00 AM – 1:00 PM'; + final reasonController = TextEditingController(); + + final slots = const [ + '9:00 AM – 11:00 AM', + '11:00 AM – 1:00 PM', + '2:00 PM – 4:00 PM', + '4:00 PM – 6:00 PM', + ]; + + @override + void initState() { + super.initState(); + newDate = widget.booking.visitDate.add(const Duration(days: 2)); + } + + @override + void dispose() { + reasonController.dispose(); + super.dispose(); + } + + Future _pickDate() async { + final picked = await showDatePicker( + context: context, + initialDate: newDate, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 90)), + ); + if (picked != null) setState(() => newDate = picked); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.75, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: ResponsiveBody( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Reschedule or Pause', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800)), + InkWell( + onTap: () => Navigator.of(context).pop(), + borderRadius: BorderRadius.circular(20), + child: const Padding(padding: EdgeInsets.all(4), child: Icon(Icons.close, size: 20)), + ), + ], + ), + const SizedBox(height: 16), + const Text('Choose an option', style: TextStyle(fontSize: 12.5, color: Colors.black54)), + const SizedBox(height: 10), + + _OptionCard( + icon: Icons.calendar_today_outlined, + title: 'Reschedule', + subtitle: 'Pick a new date & time', + selected: selected == _RescheduleOption.reschedule, + onTap: () => setState(() => selected = _RescheduleOption.reschedule), + ), + const SizedBox(height: 10), + _OptionCard( + icon: Icons.pause_circle_outline, + title: 'Pause / Hold', + subtitle: 'Pause this booking for a few days', + selected: selected == _RescheduleOption.pause, + onTap: () => setState(() => selected = _RescheduleOption.pause), + ), + const SizedBox(height: 10), + _OptionCard( + icon: Icons.skip_next_outlined, + title: 'Skip This Visit', + subtitle: 'Skip and book for later', + selected: selected == _RescheduleOption.skip, + onTap: () => setState(() => selected = _RescheduleOption.skip), + ), + + if (selected == _RescheduleOption.reschedule) ...[ + const SizedBox(height: 20), + const Text('Select new date & time', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)), + const SizedBox(height: 10), + const Text('Date', style: TextStyle(fontSize: 11.5, color: Colors.black54)), + const SizedBox(height: 4), + InkWell( + onTap: _pickDate, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(formatDate(newDate), style: const TextStyle(fontSize: 13)), + const Icon(Icons.keyboard_arrow_down, size: 18), + ], + ), + ), + ), + const SizedBox(height: 14), + const Text('Time', style: TextStyle(fontSize: 11.5, color: Colors.black54)), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(10), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: newSlot, + isExpanded: true, + icon: const Icon(Icons.keyboard_arrow_down, size: 18), + items: slots + .map((s) => DropdownMenuItem(value: s, child: Text(s, style: const TextStyle(fontSize: 13)))) + .toList(), + onChanged: (v) => setState(() => newSlot = v ?? newSlot), + ), + ), + ), + const SizedBox(height: 14), + const Text('Reason (Optional)', style: TextStyle(fontSize: 11.5, color: Colors.black54)), + const SizedBox(height: 4), + TextField( + controller: reasonController, + maxLines: 3, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + hintText: "Tell us if there's anything we should know...", + hintStyle: const TextStyle(fontSize: 12.5, color: Colors.black38), + contentPadding: const EdgeInsets.all(12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + ], + + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () { + widget.onRescheduled?.call(newDate, newSlot); + Navigator.of(context).pop(); + final msg = switch (selected) { + _RescheduleOption.reschedule => 'Booking rescheduled to ${formatDate(newDate)}', + _RescheduleOption.pause => 'Booking paused', + _RescheduleOption.skip => 'This visit will be skipped', + }; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + }, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text( + switch (selected) { + _RescheduleOption.reschedule => 'Confirm Reschedule', + _RescheduleOption.pause => 'Confirm Pause', + _RescheduleOption.skip => 'Confirm Skip', + }, + style: const TextStyle(fontWeight: FontWeight.w700), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class _OptionCard extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + final bool selected; + final VoidCallback onTap; + + const _OptionCard({ + required this.icon, + required this.title, + required this.subtitle, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: selected ? kGreenBg : Colors.white, + border: Border.all(color: selected ? kGreen : Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: selected ? Colors.white : Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 18, color: selected ? kGreen : Colors.black54), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w700)), + const SizedBox(height: 2), + Text(subtitle, style: const TextStyle(fontSize: 11.5, color: Colors.black54)), + ], + ), + ), + Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_off, + color: selected ? kGreen : Colors.grey.shade400, + size: 20, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/dashboard/data/grocery_product_model.dart b/lib/features/dashboard/data/grocery_product_model.dart new file mode 100644 index 0000000..13e7de2 --- /dev/null +++ b/lib/features/dashboard/data/grocery_product_model.dart @@ -0,0 +1,243 @@ +// Models for: +// GET /live/api/v1/mob/products/getproductsbysubcategory +// ?categoryid=&tenantid=&locationid= +// + +// Mirrors the response shape exactly (field names/casing kept as-is, e.g. +// the API's inconsistent "Subcategoryname" on the product object vs. +// "subcategoryname" on the section object). + +class GroceryProductsResponse { + final int code; + final GroceryData data; + final String message; + final bool status; + + GroceryProductsResponse({ + required this.code, + required this.data, + required this.message, + required this.status, + }); + + factory GroceryProductsResponse.fromJson(Map json) { + return GroceryProductsResponse( + code: _asInt(json['code']), + data: GroceryData.fromJson( + (json['data'] as Map?) ?? const {}), + message: (json['message'] as String?) ?? '', + status: (json['status'] as bool?) ?? false, + ); + } +} + +class GroceryData { + final String address; + final String city; + final List details; + final String licenseno; + final String locationname; + final String pickuplat; + final int pickuplocationid; + final String pickuplong; + final String postcode; + final String primarycontact; + final String primaryemail; + final String suburb; + final String tenantname; + + GroceryData({ + required this.address, + required this.city, + required this.details, + required this.licenseno, + required this.locationname, + required this.pickuplat, + required this.pickuplocationid, + required this.pickuplong, + required this.postcode, + required this.primarycontact, + required this.primaryemail, + required this.suburb, + required this.tenantname, + }); + + factory GroceryData.fromJson(Map json) { + return GroceryData( + address: (json['address'] as String?) ?? '', + city: (json['city'] as String?) ?? '', + details: ((json['details'] as List?) ?? const []) + .map((e) => GrocerySubcategory.fromJson(e as Map)) + .toList(), + licenseno: (json['licenseno'] as String?) ?? '', + locationname: (json['locationname'] as String?) ?? '', + pickuplat: (json['pickuplat'] as String?) ?? '', + pickuplocationid: _asInt(json['pickuplocationid']), + pickuplong: (json['pickuplong'] as String?) ?? '', + postcode: (json['postcode'] as String?) ?? '', + primarycontact: (json['primarycontact'] as String?) ?? '', + primaryemail: (json['primaryemail'] as String?) ?? '', + suburb: (json['suburb'] as String?) ?? '', + tenantname: (json['tenantname'] as String?) ?? '', + ); + } +} + +/// One row in `details`, e.g. "Vegetables & Fruits", "Dairy, Deli & Egg". +class GrocerySubcategory { + final int subcategoryId; + final String subcategoryName; + final String image; + final List products; + + GrocerySubcategory({ + required this.subcategoryId, + required this.subcategoryName, + required this.image, + required this.products, + }); + + factory GrocerySubcategory.fromJson(Map json) { + return GrocerySubcategory( + subcategoryId: _asInt(json['subcategoryid']), + subcategoryName: (json['subcategoryname'] as String?) ?? 'Other', + image: (json['image'] as String?) ?? '', + products: ((json['products'] as List?) ?? const []) + .map((e) => GroceryProduct.fromJson(e as Map)) + .toList(), + ); + } +} + +class GroceryProduct { + final int productId; + final int applocationId; + final int productLocationId; + final int tenantId; + final int categoryId; + final String categoryName; + final int subcategoryId; + // Note: the API returns this capitalized on the product object. + final String subcategoryNameOnProduct; + final int discountId; + final num discountValue; + final String productName; + final String productDesc; + final String? productSku; + final String? productBrand; + final String productUnit; + final String unitValue; + final String? topPicks; + final num productCost; + final num? taxAmount; + final num? taxPercent; + final num productTax; + final int productStock; + final int productCombo; + final int variants; + final int quantity; + final num? retailPrice; + final num? diffPrice; + final num? diffPercent; + final int approve; + final String productStatus; + final String? productImage; + + GroceryProduct({ + required this.productId, + required this.applocationId, + required this.productLocationId, + required this.tenantId, + required this.categoryId, + required this.categoryName, + required this.subcategoryId, + required this.subcategoryNameOnProduct, + required this.discountId, + required this.discountValue, + required this.productName, + required this.productDesc, + this.productSku, + this.productBrand, + required this.productUnit, + required this.unitValue, + this.topPicks, + required this.productCost, + this.taxAmount, + this.taxPercent, + required this.productTax, + required this.productStock, + required this.productCombo, + required this.variants, + required this.quantity, + this.retailPrice, + this.diffPrice, + this.diffPercent, + required this.approve, + required this.productStatus, + this.productImage, + }); + + /// Selling price shown to the customer: retail price if set, else cost. + num get displayPrice => retailPrice ?? productCost; + + /// Struck-through "was" price — only meaningful when the API sent a + /// positive diffprice (an actual discount amount). + num get displayOriginalPrice => + (diffPrice != null && diffPrice! > 0) ? displayPrice + diffPrice! : displayPrice; + + bool get isInStock => productStatus.toLowerCase() == 'available'; + + bool get isActive => productStatus.toLowerCase() != 'inactive'; + + factory GroceryProduct.fromJson(Map json) { + return GroceryProduct( + productId: _asInt(json['productid']), + applocationId: _asInt(json['applocationid']), + productLocationId: _asInt(json['productlocationid']), + tenantId: _asInt(json['tenantid']), + categoryId: _asInt(json['categoryid']), + categoryName: (json['categoryname'] as String?) ?? '', + subcategoryId: _asInt(json['subcategoryid']), + subcategoryNameOnProduct: (json['Subcategoryname'] as String?) ?? '', + discountId: _asInt(json['discountid']), + discountValue: _asNum(json['discountvalue']), + productName: (json['productname'] as String?) ?? 'Product', + productDesc: (json['productdesc'] as String?) ?? '', + productSku: json['productsku'] as String?, + productBrand: json['productbrand'] as String?, + productUnit: (json['productunit'] as String?) ?? '', + unitValue: (json['unitvalue'] as String?) ?? '', + topPicks: json['toppicks'] as String?, + productCost: _asNum(json['productcost']), + taxAmount: json['taxamount'] == null ? null : _asNum(json['taxamount']), + taxPercent: + json['taxpercent'] == null ? null : _asNum(json['taxpercent']), + productTax: _asNum(json['producttax']), + productStock: _asInt(json['productstock']), + productCombo: _asInt(json['productcombo']), + variants: _asInt(json['variants']), + quantity: _asInt(json['quantity']), + retailPrice: + json['retailprice'] == null ? null : _asNum(json['retailprice']), + diffPrice: json['diffprice'] == null ? null : _asNum(json['diffprice']), + diffPercent: + json['diffpercent'] == null ? null : _asNum(json['diffpercent']), + approve: _asInt(json['approve']), + productStatus: (json['productstatus'] as String?) ?? '', + productImage: json['productimage'] as String?, + ); + } +} + +int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + return int.tryParse(v.toString()) ?? 0; +} + +num _asNum(dynamic v) { + if (v == null) return 0; + if (v is num) return v; + return num.tryParse(v.toString()) ?? 0; +} \ No newline at end of file diff --git a/lib/features/dashboard/domain/repo/grocery_repository.dart b/lib/features/dashboard/domain/repo/grocery_repository.dart new file mode 100644 index 0000000..f5a52c1 --- /dev/null +++ b/lib/features/dashboard/domain/repo/grocery_repository.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +// TODO: point this at wherever your real `Product` model (the one used by +// CartController.addToCart) actually lives. +import '../../../../modules/product/product.dart'; + +/// One subcategory row returned by the API, e.g. "Vegetables & Fruits", +/// holding the real `Product` model straight from the API — the same type +/// CartController.addToCart expects, so no adapter/mapping is needed to +/// add a grocery item to the cart. +class GrocerySubcategorySection { + final int subcategoryId; + final String subcategoryName; + final String image; + final List products; + + GrocerySubcategorySection({ + required this.subcategoryId, + required this.subcategoryName, + required this.image, + required this.products, + }); +} + +class GroceryRepository { + static const String _baseUrl = + 'https://fiesta.nearle.app/live/api/v1/mob/products/getproductsbysubcategory'; + + /// Fetches grocery products for the given category/tenant/location and + /// returns them grouped by subcategory, in the order the API returns them. + /// + /// Throws an [Exception] with a readable message on failure so the caller + /// can show a retry UI. + Future> fetchGroceryProducts({ + required int categoryId, + required int tenantId, + required int locationId, + }) async { + final uri = Uri.parse( + '$_baseUrl?categoryid=$categoryId&tenantid=$tenantId&locationid=$locationId', + ); + + late final http.Response response; + try { + print('Fetching grocery products from API...'); + print(uri); + response = await http.get(uri).timeout(const Duration(seconds: 15)); + } catch (_) { + throw Exception('Could not reach the server. Check your connection.'); + } + + if (response.statusCode != 200) { + throw Exception( + 'Failed to load grocery products (HTTP ${response.statusCode})'); + } + + late final Map body; + try { + body = jsonDecode(response.body) as Map; + } catch (_) { + throw Exception('Received an unexpected response from the server.'); + } + + if (body['status'] != true) { + throw Exception( + (body['message'] as String?) ?? 'Failed to load grocery products'); + } + + final List details = + (body['data'] as Map?)?['details'] as List? ?? + []; + + return details.map((rawSection) { + final section = rawSection as Map; + final rawProducts = (section['products'] as List?) ?? []; + + return GrocerySubcategorySection( + subcategoryId: (section['subcategoryid'] as num?)?.toInt() ?? 0, + subcategoryName: (section['subcategoryname'] as String?) ?? 'Other', + image: (section['image'] as String?) ?? 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKer1bKTGDnr5Um463cjhRTpKstbPxh5DxA2hrAr1egSWr7VK42aLkiI&s=10', + // Uses your existing Product.fromJson, so tenantid/productid/etc. + // needed by addToCart are preserved straight from the API. + products: Product.fromJsonList(rawProducts), + ); + }).toList(); + } +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/ai_processing_screen.dart b/lib/features/dashboard/presentation/screens/ai_processing_screen.dart new file mode 100644 index 0000000..c87e47f --- /dev/null +++ b/lib/features/dashboard/presentation/screens/ai_processing_screen.dart @@ -0,0 +1,1424 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; +import 'package:get/get_navigation/src/routes/transitions_type.dart'; +import 'package:sleek_circular_slider/sleek_circular_slider.dart'; + +import '../../../../view/home_view.dart'; + +// ===================================================================== +// Single-file version of the "AI is finding your order" processing +// screen + the detailed order tracker screen that follows it. +// Light theme: white background, white cards, primary color used only +// for borders / icons / accents. Processing takes 10 seconds. +// ===================================================================== + +enum StepStatus { done, active, pending } + +// ----------------------------------------------------------------------- +// Shared "moving" text — a soft highlight sweeps across the text on a +// loop so headline copy feels alive instead of static. +// ----------------------------------------------------------------------- +class ShimmerText extends StatefulWidget { + final String text; + final TextStyle style; + final Color baseColor; + final Color highlightColor; + final Duration duration; + final TextAlign textAlign; + + const ShimmerText({ + super.key, + required this.text, + required this.style, + this.baseColor = const Color(0xFF1A1A1A), + this.highlightColor = const Color(0xFF662582), + this.duration = const Duration(milliseconds: 2200), + this.textAlign = TextAlign.center, + }); + + @override + State createState() => _ShimmerTextState(); +} + +class _ShimmerTextState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: widget.duration) + ..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + final t = _controller.value; + final sweep = -2.0 + 4.0 * t; + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) { + return LinearGradient( + colors: [ + widget.baseColor, + widget.baseColor, + widget.highlightColor, + widget.baseColor, + widget.baseColor, + ], + stops: const [0.0, 0.38, 0.5, 0.62, 1.0], + begin: Alignment(sweep - 0.6, 0), + end: Alignment(sweep + 0.6, 0), + ).createShader(bounds); + }, + child: Text( + widget.text, + textAlign: widget.textAlign, + style: widget.style, + ), + ); + }, + ); + } +} + +// ========================================================================= +// SCREEN 1 — AI Assistant processing screen (10 seconds) +// ========================================================================= + +class ProcessingStep { + final String title; + final String subtitle; + final IconData icon; + const ProcessingStep(this.title, this.subtitle, this.icon); +} + +class AIAssistantScreen extends StatefulWidget { + const AIAssistantScreen({super.key}); + @override + State createState() => _AIAssistantScreenState(); +} + +class _AIAssistantScreenState extends State + with TickerProviderStateMixin { + // Primary brand color used only for borders, icons, and accent text. + static const primary = Color(0xFF662582); + static const ink = Color(0xFF1A1A1A); + static const inkFaint = Color(0xFF8A8A8F); + static const bg = Color(0xFFFFFFFF); + static const cardBg = Color(0xFFFFFFFF); + static const cardBorder = Color(0xFFEDEDF2); + + late final AnimationController _progressController; + late final AnimationController _glowController; + double get _progress => _progressController.value * 100; + + final List _steps = const [ + ProcessingStep('Understanding your request', 'Analyzing your preferences', + Icons.psychology_alt_outlined), + ProcessingStep('Finding nearby restaurants', 'Scanning 120+ restaurants', + Icons.travel_explore), + ProcessingStep('Comparing ratings & prices', 'Checking reviews and prices', + Icons.compare_arrows), + ProcessingStep('Choosing the best option', 'Selecting top choice for you', + Icons.workspace_premium_outlined), + ProcessingStep('Placing your order', 'Confirming and placing order', + Icons.check_circle_outline), + ]; + + bool _navigated = false; + + @override + void initState() { + super.initState(); + + // Whole flow completes in exactly 10 seconds. + _progressController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 10000), + )..addListener(() => setState(() {})); + + _glowController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + )..repeat(reverse: true); + + _progressController.forward(); + _progressController.addStatusListener((status) { + if (status == AnimationStatus.completed && !_navigated) { + _navigated = true; + Future.delayed(const Duration(milliseconds: 550), () { + if (!mounted) return; + Get.off( + () => const OrderTrackerScreen(), + transition: Transition.rightToLeftWithFade, + duration: const Duration(milliseconds: 450), + ); + }); + } + }); + } + + @override + void dispose() { + _progressController.dispose(); + _glowController.dispose(); + super.dispose(); + } + + int get _activeIndex { + final segment = 100 / _steps.length; + final idx = (_progress / segment).floor(); + return idx.clamp(0, _steps.length - 1); + } + + StepStatus _statusFor(int index) { + if (_progress >= 100) return StepStatus.done; + if (index < _activeIndex) return StepStatus.done; + if (index == _activeIndex) return StepStatus.active; + return StepStatus.pending; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bg, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTopBar(), + const SizedBox(height: 26), + Center(child: _buildRing()), + const SizedBox(height: 24), + Center( + child: ShimmerText( + text: 'Ordering biriyani for you...', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: ink, + ), + ), + ), + const SizedBox(height: 8), + Center( + child: Text( + 'Finding the best options with great taste\nand best price', + textAlign: TextAlign.center, + style: TextStyle(color: inkFaint, fontSize: 14, height: 1.4), + ), + ), + const SizedBox(height: 24), + _buildStepsCard(), + const SizedBox(height: 16), + _buildStatsCard(), + const SizedBox(height: 16), + _buildFooterBanner(), + const SizedBox(height: 12), + ], + ), + ), + ), + ); + } + + Widget _buildTopBar() { + return Row( + children: [ + IconButton( + onPressed: () => Get.offAll(() => BottomNavigation()), + icon: const Icon(Icons.arrow_back_ios_new, color: ink, size: 20), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + Row( + children: [ + const Text('Nearle AI', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(width: 6), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: primary, width: 1), + borderRadius: BorderRadius.circular(6), + ), + child: const Text('AI', + style: TextStyle( + color: primary, + fontSize: 9, + fontWeight: FontWeight.bold)), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _glowController, + builder: (context, _) => Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: Colors.green + .withOpacity(0.5 + 0.5 * _glowController.value), + shape: BoxShape.circle, + ), + ), + ), + const SizedBox(width: 4), + Text('Processing your request', + style: TextStyle(color: inkFaint, fontSize: 12)), + ], + ), + ], + ), + const Spacer(), + const Icon(Icons.more_vert, color: ink, size: 20), + ], + ); + } + + Widget _buildRing() { + return _AiRing( + progress: _progress, + primary: primary, + centerBuilder: (value) => _buildCenter(value), + ); + } + Widget _buildCenter(double value) { + final done = value >= 100; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0.9, end: done ? 1.15 : 1.0), + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutBack, + builder: (context, scale, child) => + Transform.scale(scale: scale, child: child), + child: Text(done ? '🎉' : '🍚', style: const TextStyle(fontSize: 56)), + ), + const SizedBox(height: 8), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: '${value.toInt()}', + style: const TextStyle( + fontSize: 40, fontWeight: FontWeight.bold, color: ink), + ), + const TextSpan( + text: '%', + style: TextStyle( + fontSize: 22, fontWeight: FontWeight.bold, color: primary), + ), + ], + ), + ), + Text(done ? 'Order placed!' : 'Processing...', + style: const TextStyle(color: inkFaint)), + ], + ); + } + + Widget _buildStepsCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Column( + children: List.generate(_steps.length, (i) { + return _buildStepRow(_steps[i], _statusFor(i), i == _steps.length - 1); + }), + ), + ); + } + + Widget _buildStepRow(ProcessingStep step, StepStatus status, bool isLast) { + Color circleColor; + Widget icon; + switch (status) { + case StepStatus.done: + circleColor = primary; + icon = const Icon(Icons.check, color: Colors.white, size: 16); + break; + case StepStatus.active: + circleColor = Colors.white; + icon = Icon(step.icon, color: primary, size: 16); + break; + case StepStatus.pending: + circleColor = Colors.white; + icon = Icon(step.icon, color: inkFaint.withOpacity(0.6), size: 14); + break; + } + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + AnimatedBuilder( + animation: _glowController, + builder: (context, child) { + final pulse = status == StepStatus.active + ? 1.0 + 0.12 * _glowController.value + : 1.0; + return Transform.scale(scale: pulse, child: child); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: circleColor, + border: status == StepStatus.active + ? Border.all(color: primary, width: 2) + : status == StepStatus.pending + ? Border.all(color: cardBorder, width: 1.5) + : null, + ), + child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: KeyedSubtree(key: ValueKey(status), child: icon), + ), + ), + ), + ), + if (!isLast) + Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 2, + color: status == StepStatus.done ? primary : cardBorder, + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(step.title, + style: TextStyle( + color: status == StepStatus.pending + ? inkFaint + : ink, + fontWeight: FontWeight.w600, + fontSize: 15)), + const SizedBox(height: 4), + Text(step.subtitle, + style: TextStyle(color: inkFaint, fontSize: 13)), + ], + ), + ), + if (status == StepStatus.done) + _buildBadge('Done', Colors.green, Colors.green.withOpacity(0.08)), + if (status == StepStatus.active) + _buildBadge('${_progress.toInt()}%', primary, + primary.withOpacity(0.08)), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildBadge(String text, Color textColor, Color bgColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: textColor.withOpacity(0.3)), + ), + child: Text(text, + style: TextStyle( + color: textColor, fontSize: 12, fontWeight: FontWeight.w600)), + ); + } + + Widget _buildStatsCard() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 8), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStat(Icons.storefront, '120+', 'Restaurants\nScanned'), + _buildStat(Icons.star, '4.6+', 'Average\nRating'), + _buildStat(Icons.credit_card, '\u20b9120 - \u20b9450', 'Price\nRange'), + ], + ), + ); + } + + Widget _buildStat(IconData icon, String value, String label) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: primary.withOpacity(0.4)), + borderRadius: BorderRadius.circular(12)), + child: Icon(icon, color: primary, size: 20), + ), + const SizedBox(height: 8), + Text(value, + style: const TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 14)), + const SizedBox(height: 2), + Text(label, + textAlign: TextAlign.center, + style: const TextStyle(color: inkFaint, fontSize: 11)), + ], + ); + } + + Widget _buildFooterBanner() { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: primary.withOpacity(0.5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _glowController, + builder: (context, child) => Transform.rotate( + angle: _glowController.value * 0.6, + child: child, + ), + child: const Icon(Icons.auto_awesome, color: primary, size: 18), + ), + const SizedBox(width: 8), + Text( + _progress >= 100 + ? 'All set! Redirecting to your order...' + : 'This will only take a moment...', + style: const TextStyle(color: primary, fontWeight: FontWeight.w500), + ), + ], + ), + ); + } +} + +// ========================================================================= +// SCREEN 2 — Detailed order tracker +// ========================================================================= + +class RestaurantOption { + final String name; + final double rating; + final int price; + final String emoji; + final bool chosen; + final String reason; + + const RestaurantOption({ + required this.name, + required this.rating, + required this.price, + required this.emoji, + this.chosen = false, + this.reason = '', + }); +} + +enum TrackStage { placed, confirmed, preparing, pickedUp, onTheWay, delivered } + +class _StageInfo { + final TrackStage stage; + final String title; + final String subtitle; + final IconData icon; + const _StageInfo(this.stage, this.title, this.subtitle, this.icon); +} + +class OrderTrackerScreen extends StatefulWidget { + final String? orderId; + final String dishName; + final List? compared; + + const OrderTrackerScreen({ + super.key, + this.orderId, + this.dishName = 'Chicken Biriyani (Full)', + this.compared, + }); + + @override + State createState() => _OrderTrackerScreenState(); +} + +class _OrderTrackerScreenState extends State + with TickerProviderStateMixin { + static const primary = Color(0xFF662582); + static const ink = Color(0xFF1A1A1A); + static const inkFaint = Color(0xFF8A8A8F); + static const bg = Color(0xFFFFFFFF); + static const cardBg = Color(0xFFFFFFFF); + static const cardBorder = Color(0xFFEDEDF2); + + late final String _orderId; + late final List _compared; + + TrackStage _stage = TrackStage.confirmed; + Timer? _stageTimer; + late final AnimationController _riderController; + late final AnimationController _pulseController; + + static const List<_StageInfo> _stages = [ + _StageInfo(TrackStage.placed, 'Order Placed', 'We received your order', + Icons.receipt_long), + _StageInfo(TrackStage.confirmed, 'Restaurant Confirmed', + 'They are getting started', Icons.storefront), + _StageInfo(TrackStage.preparing, 'Food Being Prepared', + 'Cooked fresh, just for you', Icons.soup_kitchen), + _StageInfo(TrackStage.pickedUp, 'Rider Picked Up', + 'Your rider has the order', Icons.delivery_dining), + _StageInfo(TrackStage.onTheWay, 'On The Way', 'Speeding towards you', + Icons.two_wheeler), + _StageInfo(TrackStage.delivered, 'Delivered', 'Enjoy your meal!', + Icons.celebration), + ]; + + @override + void initState() { + super.initState(); + _orderId = widget.orderId ?? 'NR${100000 + math.Random().nextInt(899999)}'; + _compared = widget.compared ?? + const [ + RestaurantOption( + name: 'Paradise Biriyani House', + rating: 4.8, + price: 320, + emoji: '🍛', + chosen: true, + reason: 'Highest rating + fastest prep time'), + RestaurantOption( + name: 'Hyderabad House', rating: 4.5, price: 350, emoji: '🍚'), + RestaurantOption( + name: 'Spice Route', rating: 4.3, price: 280, emoji: '🌶️'), + ]; + + _riderController = + AnimationController(vsync: this, duration: const Duration(seconds: 2)) + ..repeat(reverse: true); + _pulseController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 1200)) + ..repeat(reverse: true); + + _stageTimer = Timer.periodic(const Duration(seconds: 4), (t) { + final nextIndex = _stage.index + 1; + if (nextIndex >= TrackStage.values.length) { + t.cancel(); + return; + } + setState(() => _stage = TrackStage.values[nextIndex]); + if (_stage == TrackStage.delivered) t.cancel(); + }); + } + + @override + void dispose() { + _stageTimer?.cancel(); + _riderController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + RestaurantOption get _chosen => + _compared.firstWhere((r) => r.chosen, orElse: () => _compared.first); + + bool get _delivered => _stage == TrackStage.delivered; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: bg, + body: SafeArea( + child: Column( + children: [ + _buildTopBar(), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + children: [ + _buildHeroCard(), + const SizedBox(height: 20), + _buildTimelineCard(), + const SizedBox(height: 20), + _buildComparisonCard(), + const SizedBox(height: 20), + _buildRiderCard(), + const SizedBox(height: 20), + _buildOrderSummary(), + const SizedBox(height: 20), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: _buildBottomBar(), + ); + } + + Widget _buildTopBar() { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 20, 4), + child: Row( + children: [ + IconButton( + onPressed: () => Get.offAll(() => BottomNavigation()), + icon: const Icon(Icons.arrow_back_ios_new, color: ink, size: 20), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Order Tracking', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 16)), + Text('#$_orderId', + style: const TextStyle(color: inkFaint, fontSize: 12)), + ], + ), + const Spacer(), + _buildLiveChip(), + ], + ), + ); + } + + Widget _buildLiveChip() { + final color = _delivered ? Colors.green : primary; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withOpacity(0.5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: _pulseController, + builder: (context, _) => Container( + width: 6, + height: 6, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color.withOpacity(0.4 + 0.6 * _pulseController.value), + ), + ), + ), + const SizedBox(width: 6), + Text(_delivered ? 'Delivered' : 'Live', + style: TextStyle( + color: color, fontSize: 11, fontWeight: FontWeight.w600)), + ], + ), + ); + } + + Widget _buildHeroCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: primary.withOpacity(0.5)), + ), + child: Column( + children: [ + ShimmerText( + text: _delivered + ? 'Your food has arrived!' + : 'Arriving in ${18 - _stage.index * 3}-${22 - _stage.index * 3} mins', + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold, color: ink), + ), + const SizedBox(height: 4), + Text( + _stages[_stage.index].subtitle, + style: const TextStyle(color: inkFaint, fontSize: 13), + ), + const SizedBox(height: 22), + SizedBox( + height: 46, + child: LayoutBuilder( + builder: (context, constraints) { + return Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: Size(constraints.maxWidth, 2), + painter: _DashedLinePainter(color: cardBorder), + ), + const Align( + alignment: Alignment.centerLeft, + child: Text('🏪', style: TextStyle(fontSize: 22)), + ), + const Align( + alignment: Alignment.centerRight, + child: Text('🏠', style: TextStyle(fontSize: 22)), + ), + AnimatedBuilder( + animation: _riderController, + builder: (context, child) { + final stageProgress = + (_stage.index / (TrackStage.values.length - 1)) + .clamp(0.05, 0.95); + final wobble = 0.04 * _riderController.value; + final x = (constraints.maxWidth - 34) * + (stageProgress + wobble).clamp(0.0, 1.0); + return Positioned( + left: x, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: primary, width: 1.5), + ), + child: const Icon(Icons.delivery_dining, + color: primary, size: 18), + ), + ); + }, + ), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTimelineCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Order Timeline', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 15)), + const SizedBox(height: 16), + ...List.generate(_stages.length, (i) { + final info = _stages[i]; + final status = i < _stage.index + ? StepStatus.done + : i == _stage.index + ? StepStatus.active + : StepStatus.pending; + return _buildTimelineRow(info, status, i == _stages.length - 1); + }), + ], + ), + ); + } + + Widget _buildTimelineRow(_StageInfo info, StepStatus status, bool isLast) { + Color circleColor; + Color iconColor; + switch (status) { + case StepStatus.done: + circleColor = primary; + iconColor = Colors.white; + break; + case StepStatus.active: + circleColor = Colors.white; + iconColor = primary; + break; + case StepStatus.pending: + circleColor = Colors.white; + iconColor = inkFaint.withOpacity(0.6); + break; + } + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + final scale = status == StepStatus.active + ? 1.0 + 0.1 * _pulseController.value + : 1.0; + return Transform.scale(scale: scale, child: child); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: circleColor, + border: status == StepStatus.pending + ? Border.all(color: cardBorder, width: 1.5) + : status == StepStatus.active + ? Border.all(color: primary, width: 2) + : null, + ), + child: Center( + child: status == StepStatus.done + ? const Icon(Icons.check, color: Colors.white, size: 16) + : Icon(info.icon, color: iconColor, size: 16), + ), + ), + ), + if (!isLast) + Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: 2, + color: status == StepStatus.done ? primary : cardBorder, + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(info.title, + style: TextStyle( + color: status == StepStatus.pending ? inkFaint : ink, + fontWeight: FontWeight.w600, + fontSize: 14)), + const SizedBox(height: 3), + Text(info.subtitle, + style: const TextStyle(color: inkFaint, fontSize: 12)), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildComparisonCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.auto_awesome, color: primary, size: 16), + const SizedBox(width: 6), + const Text('Why we picked this for you', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 15)), + ], + ), + const SizedBox(height: 4), + Text( + _chosen.reason.isEmpty + ? 'Best balance of rating and price nearby' + : _chosen.reason, + style: const TextStyle(color: inkFaint, fontSize: 12)), + const SizedBox(height: 16), + ..._compared.map((r) => _buildCompareRow(r)), + ], + ), + ); + } + + Widget _buildCompareRow(RestaurantOption r) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: r.chosen ? primary : cardBorder, width: r.chosen ? 1.4 : 1), + ), + child: Row( + children: [ + Text(r.emoji, style: const TextStyle(fontSize: 24)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text(r.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: ink, + fontWeight: FontWeight.w600, + fontSize: 13)), + ), + if (r.chosen) ...[ + const SizedBox(width: 6), + _buildBadge('Best Match', primary, primary.withOpacity(0.08)), + ], + ], + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon(Icons.star, color: primary, size: 13), + const SizedBox(width: 3), + Text(r.rating.toStringAsFixed(1), + style: const TextStyle(color: inkFaint, fontSize: 12)), + const SizedBox(width: 12), + const Icon(Icons.currency_rupee, color: inkFaint, size: 13), + Text('${r.price}', + style: const TextStyle(color: inkFaint, fontSize: 12)), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBadge(String text, Color textColor, Color bgColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: textColor.withOpacity(0.35)), + ), + child: Text(text, + style: + TextStyle(color: textColor, fontSize: 10, fontWeight: FontWeight.w600)), + ); + } + + Widget _buildRiderCard() { + final showRider = _stage.index >= TrackStage.pickedUp.index; + return AnimatedOpacity( + opacity: showRider ? 1 : 0.4, + duration: const Duration(milliseconds: 400), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Row( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all(color: primary, width: 1.5), + ), + child: const Center( + child: Text('RK', + style: TextStyle( + color: primary, fontWeight: FontWeight.bold, fontSize: 14)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Ravi Kumar', + style: TextStyle( + color: ink, fontWeight: FontWeight.w600, fontSize: 14)), + Text( + showRider + ? 'Your delivery partner · TN 07 AX 4521' + : 'Will be assigned once picked up', + style: const TextStyle(color: inkFaint, fontSize: 12), + ), + ], + ), + ), + _buildCircleIcon(Icons.call_outlined), + const SizedBox(width: 8), + _buildCircleIcon(Icons.chat_bubble_outline), + ], + ), + ), + ); + } + + Widget _buildCircleIcon(IconData icon) { + return Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: primary.withOpacity(0.5)), + ), + child: Icon(icon, color: primary, size: 16), + ); + } + + Widget _buildOrderSummary() { + const deliveryFee = 25; + final total = _chosen.price + deliveryFee; + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cardBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Order Summary', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 15)), + const SizedBox(height: 14), + Row( + children: [ + Text(_chosen.emoji, style: const TextStyle(fontSize: 20)), + const SizedBox(width: 10), + Expanded( + child: Text(widget.dishName, + style: const TextStyle(color: ink, fontSize: 13)), + ), + Text('\u20b9${_chosen.price}', + style: const TextStyle(color: inkFaint, fontSize: 13)), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + const Expanded( + child: Text('Delivery fee', + style: TextStyle(color: inkFaint, fontSize: 13))), + Text('\u20b9$deliveryFee', + style: const TextStyle(color: inkFaint, fontSize: 13)), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Divider(color: cardBorder, height: 1), + ), + Row( + children: [ + const Expanded( + child: Text('Total', + style: TextStyle( + color: ink, fontWeight: FontWeight.bold, fontSize: 15))), + Text('\u20b9$total', + style: const TextStyle( + color: primary, fontWeight: FontWeight.bold, fontSize: 16)), + ], + ), + ], + ), + ); + } + + Widget _buildBottomBar() { + return SafeArea( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: cardBorder)), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 12), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.support_agent, color: inkFaint, size: 18), + label: const Text('Need help', style: TextStyle(color: inkFaint)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + side: const BorderSide(color: cardBorder), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.call, size: 18), + label: Text(_delivered ? 'Rate order' : 'Call rider'), + style: ElevatedButton.styleFrom( + backgroundColor: primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _DashedLinePainter extends CustomPainter { + final Color color; + const _DashedLinePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 2; + const dashWidth = 6.0; + const dashSpace = 5.0; + double startX = 20; + final endX = size.width - 20; + while (startX < endX) { + canvas.drawLine( + Offset(startX, size.height / 2), + Offset(math.min(startX + dashWidth, endX), size.height / 2), + paint); + startX += dashWidth + dashSpace; + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + + + +class _AiRing extends StatefulWidget { + final double progress; // 0–100 + final Color primary; + final Widget Function(double) centerBuilder; + + const _AiRing({ + required this.progress, + required this.primary, + required this.centerBuilder, + }); + + @override + State<_AiRing> createState() => _AiRingState(); +} + +class _AiRingState extends State<_AiRing> with TickerProviderStateMixin { + late final AnimationController _pulseController; + late final AnimationController _rotateController; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + _rotateController = AnimationController( + vsync: this, + duration: const Duration(seconds: 6), + )..repeat(); + } + + @override + void dispose() { + _pulseController.dispose(); + _rotateController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: widget.progress), + duration: const Duration(milliseconds: 900), + curve: Curves.easeOutCubic, + builder: (context, animatedValue, _) { + return AnimatedBuilder( + animation: Listenable.merge([_pulseController, _rotateController]), + builder: (context, _) { + final glow = 0.35 + (_pulseController.value * 0.25); + return SizedBox( + width: 260, + height: 260, + child: Stack( + alignment: Alignment.center, + children: [ + // Soft pulsing halo + Container( + width: 260, + height: 260, + decoration: BoxDecoration( + shape: BoxShape.circle, + + ), + ), + // Slowly rotating gradient ring + Transform.rotate( + angle: _rotateController.value * 2 * math.pi, + child: CustomPaint( + size: const Size(260, 260), + painter: _RingPainter( + progress: animatedValue, + primary: widget.primary, + ), + ), + ), + widget.centerBuilder(animatedValue), + ], + ), + ); + }, + ); + }, + ); + } +} + +class _RingPainter extends CustomPainter { + final double progress; + final Color primary; + + _RingPainter({required this.progress, required this.primary}); + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + final radius = size.width / 2 - 12; + + // Track + final trackPaint = Paint() + ..color = primary.withOpacity(0.08) + ..strokeWidth = 10 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + canvas.drawCircle(center, radius, trackPaint); + + final sweepAngle = 2 * math.pi * (progress / 100); + final rect = Rect.fromCircle(center: center, radius: radius); + + final gradient = SweepGradient( + startAngle: 0, + endAngle: sweepAngle == 0 ? 0.001 : sweepAngle, + colors: [ + primary.withOpacity(0.2), + primary, + Colors.pinkAccent.shade100, + primary, + ], + stops: const [0.0, 0.4, 0.7, 1.0], + transform: const GradientRotation(-math.pi / 2), + ); + + // Blurred glow pass + final glowPaint = Paint() + ..shader = gradient.createShader(rect) + ..strokeWidth = 18 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8); + + // Crisp pass on top + final progressPaint = Paint() + ..shader = gradient.createShader(rect) + ..strokeWidth = 12 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + canvas.drawArc(rect, -math.pi / 2, sweepAngle, false, glowPaint); + canvas.drawArc(rect, -math.pi / 2, sweepAngle, false, progressPaint); + + // Leading "spark" dot + if (progress > 0) { + final dotAngle = -math.pi / 2 + sweepAngle; + final dotOffset = Offset( + center.dx + radius * math.cos(dotAngle), + center.dy + radius * math.sin(dotAngle), + ); + canvas.drawCircle( + dotOffset, + 7, + Paint() + ..color = primary + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6), + ); + canvas.drawCircle(dotOffset, 4, Paint()..color = Colors.white); + } + } + + @override + bool shouldRepaint(covariant _RingPainter oldDelegate) { + return oldDelegate.progress != progress || oldDelegate.primary != primary; + } +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/camera_search.dart b/lib/features/dashboard/presentation/screens/camera_search.dart new file mode 100644 index 0000000..37f3882 --- /dev/null +++ b/lib/features/dashboard/presentation/screens/camera_search.dart @@ -0,0 +1,526 @@ +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../../../modules/search_model/search_model.dart'; +import '../../../Products/product_card.dart'; +import 'edit_object.dart'; + + + +/// Full-screen camera-based visual product search, styled after +/// Google Lens / Amazon-style "search by image" camera views. +/// +/// Capturing or picking a photo now hands off to [CropConfirmScreen], +/// which shows a draggable crop box (pre-filled from on-device object +/// detection) so the user can pick exactly which product to search for. +class LensSearchScreen extends StatefulWidget { + const LensSearchScreen({super.key}); + + @override + State createState() => _LensSearchScreenState(); +} + +class _LensSearchScreenState extends State + with WidgetsBindingObserver { + CameraController? _controller; + List _cameras = []; + bool _flashOn = false; + bool _isInitializing = true; + String? _error; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _setupCamera(); + } + + Future _setupCamera() async { + try { + _cameras = await availableCameras(); + if (_cameras.isEmpty) { + setState(() { + _error = 'No camera found on this device.'; + _isInitializing = false; + }); + return; + } + + final backCamera = _cameras.firstWhere( + (cam) => cam.lensDirection == CameraLensDirection.back, + orElse: () => _cameras.first, + ); + + final controller = CameraController( + backCamera, + ResolutionPreset.high, + enableAudio: false, + ); + + await controller.initialize(); + if (!mounted) return; + + setState(() { + _controller = controller; + _isInitializing = false; + }); + } catch (e) { + setState(() { + _error = 'Could not start camera: $e'; + _isInitializing = false; + }); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final controller = _controller; + if (controller == null || !controller.value.isInitialized) return; + + if (state == AppLifecycleState.inactive) { + controller.dispose(); + } else if (state == AppLifecycleState.resumed) { + _setupCamera(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _controller?.dispose(); + super.dispose(); + } + + Future _toggleFlash() async { + final controller = _controller; + if (controller == null) return; + final next = !_flashOn; + try { + await controller.setFlashMode(next ? FlashMode.torch : FlashMode.off); + setState(() => _flashOn = next); + } catch (_) { + // Some devices/emulators don't support torch mode; ignore silently. + } + } + + Future _capturePhoto() async { + final controller = _controller; + if (controller == null || !controller.value.isInitialized) return; + if (controller.value.isTakingPicture) return; + + try { + final file = await controller.takePicture(); + if (!mounted) return; + _goToCropConfirm(file.path); + } catch (e) { + _showSnack('Failed to capture photo: $e'); + } + } + + Future _pickFromGallery() async { + final picker = ImagePicker(); + final picked = await picker.pickImage(source: ImageSource.gallery); + if (picked == null) return; + if (!mounted) return; + _goToCropConfirm(picked.path); + } + + void _goToCropConfirm(String imagePath) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CropConfirmScreen(imagePath: imagePath), + ), + ); + } + + void _showSnack(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + fit: StackFit.expand, + children: [ + _buildCameraPreview(), + + Positioned( + top: 0, + left: 0, + right: 0, + height: 140, + child: IgnorePointer( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black54, Colors.transparent], + ), + ), + ), + ), + ), + + SafeArea( + child: Column( + children: [ + _buildTopBar(), + const Spacer(), + _buildHintPill(), + const SizedBox(height: 24), + _buildBottomBar(), + const SizedBox(height: 12), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCameraPreview() { + if (_isInitializing) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + if (_error != null || _controller == null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + _error ?? 'Camera unavailable', + style: const TextStyle(color: Colors.white70), + textAlign: TextAlign.center, + ), + ), + ); + } + return FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: _controller!.value.previewSize?.height ?? 1, + height: _controller!.value.previewSize?.width ?? 1, + child: CameraPreview(_controller!), + ), + ); + } + + Widget _buildTopBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + const Expanded(child: _LensWordmark()), + _CircleIconButton( + icon: _flashOn ? Icons.flash_on : Icons.flash_on_outlined, + onTap: _toggleFlash, + ), + const SizedBox(width: 14), + _CircleIconButton( + icon: Icons.help_outline, + onTap: () => + _showSnack('Point the camera at a product to search for it.'), + ), + ], + ), + ); + } + + Widget _buildHintPill() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.55), + borderRadius: BorderRadius.circular(24), + ), + child: const Text( + 'Take a photo to search products', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ); + } + + Widget _buildBottomBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _CircleIconButton( + icon: Icons.add_photo_alternate_outlined, + onTap: _pickFromGallery, + size: 48, + ), + _ShutterButton(onTap: _capturePhoto), + _CircleIconButton( + icon: Icons.qr_code_scanner, + onTap: () => _showSnack('Barcode scan mode coming soon.'), + size: 48, + ), + ], + ), + ); + } +} + +class _LensWordmark extends StatelessWidget { + const _LensWordmark(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + 'Nearle', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(width: 6), + Text( + 'ai', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(width: 2), + Icon(Icons.auto_awesome, color: Colors.white, size: 14), + ], + ); + } +} + +class _CircleIconButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + final double size; + + const _CircleIconButton({ + required this.icon, + required this.onTap, + this.size = 36, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.black.withOpacity(0.35), + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: onTap, + child: SizedBox( + width: size, + height: size, + child: Icon(icon, color: Colors.white, size: size * 0.5), + ), + ), + ); + } +} + +class _ShutterButton extends StatelessWidget { + final VoidCallback onTap; + + const _ShutterButton({required this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 76, + height: 76, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all(color: Colors.white54, width: 4), + ), + child: const Icon(Icons.search, color: Colors.black87, size: 30), + ), + ); + } +} + +/// Results screen shown after crop confirmation. +/// +/// [extractedText] is whatever on-device OCR (ML Kit Text Recognition) +/// found inside the user's cropped selection — e.g. a brand name like +/// "OPPO", model numbers, or other label text. It's shown in a dedicated +/// card with a "Copy" button so the user can grab it (paste into notes, +/// a search bar, a print dialog, wherever). +/// +/// Replace `_fetchMatches` with a real call to your product-search API. +class SearchResultsScreen extends StatelessWidget { + final String imagePath; + final bool wasDetected; + final String extractedText; + + const SearchResultsScreen({ + super.key, + required this.imagePath, + this.wasDetected = false, + this.extractedText = '', + }); + + Future _copyText(BuildContext context) async { + await Clipboard.setData(ClipboardData(text: extractedText)); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Text copied to clipboard')), + ); + } + + /// Placeholder matches until this is wired to a real product-search API. + /// Swap this out for the actual API response mapped into [ProductItem]s. + /// NOTE: adjust field names here if they don't match your ProductItem + /// constructor exactly (image, category, name, rating, reviewCount, + /// price, originalPrice, isFavorite are assumed based on ProductCard). + List _mockMatches() { + return List.generate(10, (index) { + final price = 15 + index * 7.5; + final original = price + 10; + return ProductItem( + image: 'https://picsum.photos/seed/product$index/300/300', + category: 'Electronics', + name: 'Matched product ${index + 1}', + rating: 4.5, + reviewCount: 20 + index, + price: '\$${price.toStringAsFixed(2)}', + originalPrice: '\$${original.toStringAsFixed(2)}', + isFavorite: false, + ); + }); + } + + @override + Widget build(BuildContext context) { + final hasText = extractedText.trim().isNotEmpty; + final matches = _mockMatches(); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, + animateColor: false, + title: const Text('Search results')), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(imagePath), + width: 56, + height: 56, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: 56, + height: 56, + color: Colors.grey.shade300, + child: const Icon(Icons.image), + ), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Showing matches for your cropped selection', + style: TextStyle(fontSize: 14, color: Colors.black54), + ), + ), + ], + ), + ), + + if (hasText) _buildDetectedTextCard(context), + + const SizedBox(height: 8), + + Expanded( + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 0.72, + ), + itemCount: matches.length, + itemBuilder: (context, index) => ProductCard(item: matches[index]), + ), + ), + ], + ), + ); + } + + Widget _buildDetectedTextCard(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.text_snippet_outlined, + size: 18, color: Colors.grey.shade700), + const SizedBox(width: 6), + const Text( + 'Detected text', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14), + ), + const Spacer(), + TextButton.icon( + onPressed: () => _copyText(context), + icon: const Icon(Icons.copy, size: 16), + label: const Text('Copy'), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + const SizedBox(height: 6), + SelectableText( + extractedText, + style: const TextStyle(fontSize: 14, height: 1.4), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/daily_plan.dart b/lib/features/dashboard/presentation/screens/daily_plan.dart new file mode 100644 index 0000000..1218757 --- /dev/null +++ b/lib/features/dashboard/presentation/screens/daily_plan.dart @@ -0,0 +1,1690 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + + +// ───────────────────────────────────────── +// MODELS +// ───────────────────────────────────────── + +class Product { + final String id; + final String name; + final String quantity; + final double price; + final String emoji; + final String category; + final bool isPopular; + int count; + bool isSelected; + + Product({ + required this.id, + required this.name, + required this.quantity, + required this.price, + required this.emoji, + required this.category, + this.isPopular = false, + this.count = 0, + this.isSelected = false, + }); +} + +class Store { + final String name; + final double rating; + final String deliveryTime; + final String distance; + final Color color; + + const Store({ + required this.name, + required this.rating, + required this.deliveryTime, + required this.distance, + required this.color, + }); +} + +// ───────────────────────────────────────── +// DATA +// ───────────────────────────────────────── + +final List availableStores = [ + Store(name: 'Fresh Mart', rating: 4.8, deliveryTime: '20-30 mins', distance: '2.1 km away', color: Color(0xFF1A7A3C)), + Store(name: 'Green Basket', rating: 4.5, deliveryTime: '25-35 mins', distance: '3.4 km away', color: Color(0xFF2E7D32)), + Store(name: 'Daily Needs', rating: 4.3, deliveryTime: '30-45 mins', distance: '1.8 km away', color: Color(0xFF1565C0)), + Store(name: 'Super Saver', rating: 4.6, deliveryTime: '15-25 mins', distance: '4.0 km away', color: Color(0xFFE65100)), +]; + +List getFrequentProducts() => [ + Product(id: 'milk', name: 'Milk', quantity: '1 Litre', price: 56, emoji: '🥛', category: 'Milk & Dairy', isPopular: true, count: 1, isSelected: true), + Product(id: 'eggs', name: 'Eggs', quantity: '6 Pieces', price: 36, emoji: '🥚', category: 'Milk & Dairy', isPopular: true, count: 1, isSelected: true), + Product(id: 'bread', name: 'Bread', quantity: '1 Pack', price: 30, emoji: '🍞', category: 'Bakery', count: 0, isSelected: false), + Product(id: 'banana', name: 'Banana', quantity: '6 Pieces', price: 28, emoji: '🍌', category: 'Fruits', count: 1, isSelected: true), + Product(id: 'oats', name: 'Oats', quantity: '500 g', price: 80, emoji: '🌾', category: 'Staples', count: 0, isSelected: false), +]; + +Map> getAllProducts() => { + 'Milk & Dairy': [ + Product(id: 'fresh_milk', name: 'Fresh Milk', quantity: '1 Litre', price: 56, emoji: '🥛', category: 'Milk & Dairy', isPopular: true), + Product(id: 'curd', name: 'Curd', quantity: '500 g', price: 28, emoji: '🫙', category: 'Milk & Dairy', isPopular: true), + Product(id: 'butter', name: 'Butter', quantity: '100 g', price: 62, emoji: '🧈', category: 'Milk & Dairy'), + Product(id: 'cheese', name: 'Cheese', quantity: '200 g', price: 110, emoji: '🧀', category: 'Milk & Dairy'), + Product(id: 'paneer', name: 'Paneer', quantity: '200 g', price: 90, emoji: '🟨', category: 'Milk & Dairy'), + Product(id: 'dahi', name: 'Dahi', quantity: '200 g', price: 22, emoji: '🫙', category: 'Milk & Dairy'), + Product(id: 'buttermilk', name: 'Buttermilk', quantity: '500 ml', price: 18, emoji: '🥛', category: 'Milk & Dairy'), + Product(id: 'fresh_cream', name: 'Fresh Cream', quantity: '200 ml', price: 45, emoji: '🍦', category: 'Milk & Dairy'), + ], + 'Vegetables': [ + Product(id: 'tomato', name: 'Tomato', quantity: '500 g', price: 25, emoji: '🍅', category: 'Vegetables', isPopular: true), + Product(id: 'onion', name: 'Onion', quantity: '1 kg', price: 35, emoji: '🧅', category: 'Vegetables'), + Product(id: 'potato', name: 'Potato', quantity: '1 kg', price: 30, emoji: '🥔', category: 'Vegetables', isPopular: true), + Product(id: 'spinach', name: 'Spinach', quantity: '250 g', price: 20, emoji: '🥬', category: 'Vegetables'), + Product(id: 'carrot', name: 'Carrot', quantity: '500 g', price: 30, emoji: '🥕', category: 'Vegetables'), + Product(id: 'capsicum', name: 'Capsicum', quantity: '250 g', price: 28, emoji: '🫑', category: 'Vegetables'), + ], + 'Fruits': [ + Product(id: 'apple', name: 'Apple', quantity: '4 Pieces', price: 80, emoji: '🍎', category: 'Fruits', isPopular: true), + Product(id: 'banana_f', name: 'Banana', quantity: '6 Pieces', price: 28, emoji: '🍌', category: 'Fruits', isPopular: true), + Product(id: 'mango', name: 'Mango', quantity: '2 Pieces', price: 60, emoji: '🥭', category: 'Fruits'), + Product(id: 'grapes', name: 'Grapes', quantity: '500 g', price: 70, emoji: '🍇', category: 'Fruits'), + Product(id: 'orange', name: 'Orange', quantity: '4 Pieces', price: 55, emoji: '🍊', category: 'Fruits'), + ], + 'Staples': [ + Product(id: 'rice', name: 'Rice', quantity: '1 kg', price: 55, emoji: '🍚', category: 'Staples', isPopular: true), + Product(id: 'dal', name: 'Dal', quantity: '500 g', price: 65, emoji: '🫘', category: 'Staples'), + Product(id: 'oats_s', name: 'Oats', quantity: '500 g', price: 80, emoji: '🌾', category: 'Staples'), + Product(id: 'atta', name: 'Atta', quantity: '1 kg', price: 45, emoji: '🌾', category: 'Staples'), + ], + 'Snacks': [ + Product(id: 'chips', name: 'Chips', quantity: '100 g', price: 20, emoji: '🥨', category: 'Snacks'), + Product(id: 'biscuits', name: 'Biscuits', quantity: '200 g', price: 35, emoji: '🍪', category: 'Snacks'), + Product(id: 'namkeen', name: 'Namkeen', quantity: '200 g', price: 40, emoji: '🫘', category: 'Snacks'), + ], + 'Beverages': [ + Product(id: 'tea', name: 'Tea', quantity: '250 g', price: 70, emoji: '🍵', category: 'Beverages', isPopular: true), + Product(id: 'coffee', name: 'Coffee', quantity: '100 g', price: 120, emoji: '☕', category: 'Beverages'), + Product(id: 'juice', name: 'Juice', quantity: '1 L', price: 85, emoji: '🧃', category: 'Beverages'), + ], + 'Bakery': [ + Product(id: 'bread_b', name: 'Bread', quantity: '1 Pack', price: 30, emoji: '🍞', category: 'Bakery', isPopular: true), + Product(id: 'bun', name: 'Bun', quantity: '6 Pieces', price: 25, emoji: '🥐', category: 'Bakery'), + ], +}; + +// ───────────────────────────────────────── +// CONSTANTS +// ───────────────────────────────────────── + +const kGreen = Color(0xFF1A7A3C); +const kGreenLight = Color(0xFFE8F5EE); +const kBg = Color(0xFFF5F7F5); +const kText = Color(0xFF1C1C1C); + +// ───────────────────────────────────────── +// APP +// ───────────────────────────────────────── + + + +// ───────────────────────────────────────── +// SCREEN 1 — CREATE DAILY PLAN +// ───────────────────────────────────────── + +class CreateDailyPlanScreen extends StatefulWidget { + const CreateDailyPlanScreen({super.key}); + + @override + State createState() => _CreateDailyPlanScreenState(); +} + +class _CreateDailyPlanScreenState extends State { + Store _store = availableStores[0]; + late List _frequentProducts; + String _deliveryTime = 'Morning'; + Set _deliveryDays = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri'}; + String _selectedCategory = 'All'; + bool _agreedToTerms = false; + +// Add these to your state variables: + DateTime _startDate = DateTime.now(); + DateTime? _endDate; + bool _noEndDate = false; + final List> _timeSlots = [ + {'label': 'Morning', 'time': '6 AM – 9 AM', 'icon': '🌅', 'popular': true}, + {'label': 'Forenoon', 'time': '9 AM – 12 PM', 'icon': '☀️', 'popular': false}, + {'label': 'Afternoon', 'time': '12 PM – 4 PM', 'icon': '🌤️', 'popular': false}, + {'label': 'Evening', 'time': '4 PM – 8 PM', 'icon': '🌇', 'popular': false}, + {'label': 'Night', 'time': '8 PM – 10 PM', 'icon': '🌙', 'popular': false}, + ]; + + final List _allDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + final List _categoryTabs = ['All', 'Milk & Dairy', 'Vegetables', 'Fruits', 'Staples', 'Snacks', 'More']; + + @override + void initState() { + super.initState(); + _frequentProducts = getFrequentProducts(); + } + + double get _dailyTotal => + _frequentProducts.where((p) => p.count > 0).fold(0, (s, p) => s + p.price * p.count); + + int get _selectedCount => _frequentProducts.where((p) => p.count > 0).length; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: kBg, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 12), + _buildCard(_buildStoreSection()), + const SizedBox(height: 12), + _buildCard(_buildItemsSection()), + const SizedBox(height: 12), + _buildCard(_buildDeliveryTimeSection()), + const SizedBox(height: 12), + _buildCard(_buildDeliveryDaysSection()), + const SizedBox(height: 12), + _buildCard(_buildSummarySection()), + const SizedBox(height: 8), + ], + ), + ), + ), + _buildBottomBar(), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + + padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), + child: Row( + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Icon(Icons.arrow_back, size: 22, color: kText), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Create Daily Plan', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: kText)), + Text('Set your everyday essentials, we\'ll deliver on time.', + style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ), + const Text('🛒', style: TextStyle(fontSize: 34)), + ], + ), + ); + } + + Widget _buildCard(Widget child) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2))], + ), + child: child, + ); + } + + // ── 1. Store section ── + Widget _buildStoreSection() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('1. Choose Store', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: kText)), + const SizedBox(height: 12), + Row( + children: [ + Container( + width: 52, height: 52, + decoration: BoxDecoration(color: _store.color, borderRadius: BorderRadius.circular(12)), + child: const Icon(Icons.shopping_basket, color: Colors.white, size: 26), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_store.name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: kText)), + const SizedBox(height: 3), + Row( + children: [ + const Icon(Icons.star, color: Color(0xFFFFB300), size: 14), + const SizedBox(width: 3), + Text('${_store.rating}', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: kText)), + const SizedBox(width: 5), + Container(width: 3, height: 3, + decoration: const BoxDecoration(color: Colors.grey, shape: BoxShape.circle)), + const SizedBox(width: 5), + Text(_store.deliveryTime, + style: const TextStyle(fontSize: 13, color: Colors.grey)), + ], + ), + Text(_store.distance, style: const TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ), + OutlinedButton( + onPressed: _openChangeStore, + style: OutlinedButton.styleFrom( + side: const BorderSide(color: kGreen), + foregroundColor: kGreen, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const Row( + children: [ + Text('Change Store', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), + SizedBox(width: 4), + Icon(Icons.chevron_right, size: 16), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration(color: kGreenLight, borderRadius: BorderRadius.circular(10)), + child: Row( + children: [ + const Icon(Icons.info_outline, color: kGreen, size: 16), + const SizedBox(width: 8), + RichText( + text: const TextSpan( + style: TextStyle(fontSize: 12, color: Colors.black87), + children: [ + TextSpan(text: 'Your daily plan will be created from this '), + TextSpan(text: 'store only.', + style: TextStyle(color: kGreen, fontWeight: FontWeight.w600)), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + // ── 2. Items section ── + Widget _buildItemsSection() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('2. Add items to your daily plan', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: kText)), + Text('Choose items you need regularly from ${_store.name}', + style: const TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 14), + // Category tabs + SizedBox( + height: 70, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _categoryTabs.length, + itemBuilder: (context, i) { + final tab = _categoryTabs[i]; + final isSelected = tab == _selectedCategory; + final emojis = {'All': '⊞', 'Milk & Dairy': '🥛', 'Vegetables': '🥦', + 'Fruits': '🍎', 'Staples': '🫙', 'Snacks': '🍟', 'More': '›'}; + return GestureDetector( + onTap: () => setState(() => _selectedCategory = tab), + child: Container( + margin: const EdgeInsets.only(right: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 44, height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? kGreenLight : const Color(0xFFF5F5F5), + border: isSelected ? Border.all(color: kGreen, width: 2) : null, + ), + child: Center( + child: Text(emojis[tab] ?? '📦', + style: const TextStyle(fontSize: 20)), + ), + ), + const SizedBox(height: 4), + Text(tab, + style: TextStyle( + fontSize: 10, + color: isSelected ? kGreen : Colors.grey[600], + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + )), + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 14), + // Frequently Bought heading + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Your Frequently Bought from ${_store.name}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: kText)), + GestureDetector( + onTap: _openAddProducts, + child: const Row( + children: [ + Icon(Icons.add, color: kGreen, size: 16), + Text(' Add all', style: TextStyle(color: kGreen, fontSize: 13, fontWeight: FontWeight.w600)), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + // Products row + SizedBox( + height: 168, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _frequentProducts.length, + itemBuilder: (context, i) => _buildFrequentProductCard(_frequentProducts[i]), + ), + ), + const SizedBox(height: 12), + // Smart suggestion bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEEEEEE)), + ), + child: Row( + children: [ + const Text('🛒', style: TextStyle(fontSize: 22)), + const SizedBox(width: 10), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Smart Suggestion', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: kText)), + Text('Based on your past orders, we\'ve suggested these essentials.', + style: TextStyle(fontSize: 11, color: Colors.grey)), + ], + ), + ), + GestureDetector( + onTap: _openAddProducts, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFDDDDDD)), + borderRadius: BorderRadius.circular(8), + ), + child: const Text('View all', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: kText)), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFrequentProductCard(Product product) { + final isAdded = product.count > 0; + return Container( + + + width: 120, + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: isAdded ? kGreen : const Color(0xFFE0E0E0), width: isAdded ? 2 : 1), + color: Colors.white, + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text(product.emoji, style: const TextStyle(fontSize: 42))), + const SizedBox(height: 4), + Row( + children: [ + Text(product.name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 10, color: kText)), + SizedBox(width: 7), + Text(product.quantity, style: const TextStyle(fontSize: 7, color: Colors.black54)), + + ], + ), + const SizedBox(height: 2), + Text('₹${product.price.toStringAsFixed(0)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: kText)), + const SizedBox(height: 6), + // Counter + Container( + height: 20, + decoration: BoxDecoration( + border: Border.all(color: isAdded ? kGreen : const Color(0xFFDDDDDD)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _counterBtn(Icons.remove, () { + setState(() { if (product.count > 0) product.count--; }); + }), + Text('${product.count}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: isAdded ? kGreen : Colors.grey, + )), + _counterBtn(Icons.add, () { + setState(() => product.count++); + }), + ], + ), + ), + ], + ), + ), + + ], + ), + ); + } + + Widget _counterBtn(IconData icon, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Icon(icon, size: 14, color: kGreen), + ), + ); + } + + // ── 3. Delivery time ── + Widget _buildDeliveryTimeSection() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('3. Choose delivery time', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: kText)), + const Text('We\'ll deliver your items daily', + style: TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 14), + SizedBox( + height: 90, // ← reduced from 130 + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _timeSlots.length, + itemBuilder: (context, i) { + final slot = _timeSlots[i]; + final isSelected = slot['label'] == _deliveryTime; + return GestureDetector( + onTap: () => setState(() => _deliveryTime = slot['label']), + child: Container( + width: 80, // ← reduced from 100 + margin: const EdgeInsets.only(right: 8), // ← reduced from 10 + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), // ← reduced from 12 + border: Border.all( + color: isSelected ? kGreen : const Color(0xFFE0E0E0), + width: isSelected ? 2 : 1, + ), + color: isSelected ? kGreenLight : Colors.white, + ), + child: Padding( + padding: const EdgeInsets.all(6), // ← reduced from 10 + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(slot['icon'], style: const TextStyle(fontSize: 18)), // ← reduced from 22 + const SizedBox(height: 2), + Text(slot['label'], + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11, // ← reduced from 12 + color: isSelected ? kGreen : kText, + )), + Text(slot['time'], + style: const TextStyle(fontSize: 8, color: Colors.grey), // ← reduced from 9 + textAlign: TextAlign.center), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 10), + ], + ), + ); + } + // ── 4. Delivery days ── + Widget _buildDeliveryDaysSection() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '4. Select delivery days', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: kText, + ), + ), + const Text( + 'On which days should we deliver?', + style: TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 14), + + // Days + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _allDays.map((day) { + final isSelected = _deliveryDays.contains(day); + + return GestureDetector( + onTap: () { + setState(() { + if (isSelected) { + _deliveryDays.remove(day); + } else { + _deliveryDays.add(day); + } + }); + }, + child: Container( + width: 44, + height: 58, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: isSelected + ? kGreenLight + : const Color(0xFFF5F5F5), + border: Border.all( + color: isSelected + ? kGreen + : Colors.transparent, + width: isSelected ? 1.5 : 0, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + day, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: isSelected + ? kGreen + : Colors.grey[700], + ), + ), + const SizedBox(height: 4), + Icon( + isSelected + ? Icons.check_circle + : Icons.circle_outlined, + color: isSelected + ? kGreen + : Colors.grey[300], + size: 16, + ), + ], + ), + ), + ); + }).toList(), + ), + + const SizedBox(height: 20), + + const Text( + 'Duration', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: kText, + ), + ), + + const SizedBox(height: 10), + + Row( + children: [ + Expanded( + child: _buildDateField( + label: 'Start date', + date: _startDate, + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: _startDate, + firstDate: DateTime.now(), + lastDate: DateTime(2100), + ); + + if (picked != null) { + setState(() { + _startDate = picked; + + if (_endDate != null && + _endDate!.isBefore(_startDate)) { + _endDate = _startDate + .add(const Duration(days: 30)); + } + }); + } + }, + ), + ), + + const SizedBox(width: 12), + + Expanded( + child: _buildDateField( + label: 'End date', + date: _noEndDate ? null : _endDate, + enabled: !_noEndDate, + onTap: _noEndDate + ? null + : () async { + final initialDate = + (_endDate != null && + !_endDate!.isBefore(_startDate)) + ? _endDate! + : _startDate.add( + const Duration(days: 30), + ); + + final picked = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: _startDate, + lastDate: DateTime(2100), + ); + + if (picked != null) { + setState(() { + _endDate = picked; + }); + } + }, + ), + ), + ], + ), + + const SizedBox(height: 12), + + GestureDetector( + onTap: () { + setState(() { + _noEndDate = !_noEndDate; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _noEndDate + ? kGreen + : Colors.grey.shade300, + width: _noEndDate ? 1.5 : 1, + ), + color: _noEndDate + ? kGreenLight + : Colors.white, + ), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: _noEndDate + ? kGreen + : Colors.grey.shade400, + width: 1.5, + ), + color: _noEndDate + ? kGreen + : Colors.white, + ), + child: _noEndDate + ? const Icon( + Icons.check, + size: 14, + color: Colors.white, + ) + : null, + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.all_inclusive, + size: 16, + color: Colors.grey, + ), + const SizedBox(width: 4), + Text( + 'No end date', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _noEndDate + ? kGreen + : kText, + ), + ), + ], + ), + const Text( + 'Continue until you pause or cancel', + style: TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +// Helper — add this as a separate method in your widget class + Widget _buildDateField({ + required String label, + required DateTime? date, + bool enabled = true, + VoidCallback? onTap, + }) { + final formatter = DateFormat('d MMM yyyy'); // import 'package:intl/intl.dart' + return GestureDetector( + onTap: enabled ? onTap : null, + child: Container( + padding: const EdgeInsets.fromLTRB(12, 8, 10, 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: enabled ? kGreen : Colors.grey[300]!, width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + fontSize: 10, + color: enabled ? kGreen : Colors.grey, + fontWeight: FontWeight.w500, + )), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.calendar_today_outlined, size: 14, color: Colors.grey[600]), + const SizedBox(width: 6), + Expanded( + child: Text( + date != null ? formatter.format(date) : '—', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: enabled ? kText : Colors.grey[400], + ), + ), + ), + Icon(Icons.chevron_right, size: 16, color: Colors.grey[400]), + ], + ), + ], + ), + ), + ); + } // ── Summary ── + + + Widget _buildSummarySection() { + final selectedDaysStr = _deliveryDays.join(', '); + final timeSlot = _timeSlots.firstWhere((s) => s['label'] == _deliveryTime); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Your Daily Plan Summary', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: kText)), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: kGreenLight, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _summaryChip('🛒', '$_selectedCount Items'), + _divider(), + _summaryChip('🌅', '${timeSlot['label']}\n${timeSlot['time']}'), + _divider(), + _summaryChip('📅', selectedDaysStr), + ], + ), + ), + SizedBox( + height: 20, + ) + ], + ), + ); + } + + Widget _summaryChip(String emoji, String text) { + return Expanded( + child: Column( + children: [ + Text(emoji, style: const TextStyle(fontSize: 20)), + const SizedBox(height: 4), + Text(text, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: kText)), + ], + ), + ); + } + + Widget _divider() => Container(height: 36, width: 1, color: kGreen.withOpacity(0.2)); + + // ── Bottom bar ── + Widget _buildBottomBar() { + return Container( + color: Colors.white, + padding: EdgeInsets.fromLTRB(16, 10, 16, MediaQuery.of(context).padding.bottom + 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Checkbox( + value: _agreedToTerms, + onChanged: (v) => setState(() => _agreedToTerms = v ?? false), + activeColor: kGreen, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + Flexible( + child: RichText( + text: const TextSpan( + style: TextStyle(fontSize: 12, color: Colors.black87), + children: [ + TextSpan(text: 'I agree to the Auto Daily Plan '), + TextSpan(text: 'terms and conditions', + style: TextStyle(color: kGreen, fontWeight: FontWeight.w600)), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Estimated daily total ₹${_dailyTotal.toStringAsFixed(0)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: kText)), + const SizedBox(width: 4), + const Icon(Icons.info_outline, size: 14, color: Colors.grey), + ], + ), + const Text('You can edit anytime', + style: TextStyle(fontSize: 11, color: Colors.grey)), + ], + ), + ElevatedButton( + onPressed: _agreedToTerms ? () {} : null, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + disabledBackgroundColor: Colors.grey[300], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: const Text('Create Daily Plan', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), + ), + ], + ), + ], + ), + ); + } + + // ── Open sheets ── + void _openChangeStore() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ChangeStoreSheet( + selectedStore: _store, + onStoreSelected: (s) => setState(() => _store = s), + ), + ); + } + + void _openAddProducts() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => AddProductsSheet( + selectedProducts: _frequentProducts.where((p) => p.isSelected).toList(), + onProductsAdded: (products) => setState(() { + // Merge selections back + for (final p in products) { + final existing = _frequentProducts.indexWhere((fp) => fp.id == p.id); + if (existing == -1) { + _frequentProducts.add(p..count = 1); + } else { + _frequentProducts[existing].isSelected = true; + if (_frequentProducts[existing].count == 0) { + _frequentProducts[existing].count = 1; + } + } + } + }), + ), + ); + } +} + +// ───────────────────────────────────────── +// SHEET — CHANGE STORE +// ───────────────────────────────────────── + +class ChangeStoreSheet extends StatefulWidget { + final Store selectedStore; + final Function(Store) onStoreSelected; + + const ChangeStoreSheet({super.key, required this.selectedStore, required this.onStoreSelected}); + + @override + State createState() => _ChangeStoreSheetState(); +} + +class _ChangeStoreSheetState extends State { + late Store _selected; + final TextEditingController _searchCtrl = TextEditingController(); + List _filtered = availableStores; + + @override + void initState() { + super.initState(); + _selected = widget.selectedStore; + } + + void _filter(String q) => setState(() { + _filtered = availableStores.where((s) => s.name.toLowerCase().contains(q.toLowerCase())).toList(); + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle + Container( + margin: const EdgeInsets.only(top: 12), + width: 40, height: 4, + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), + ), + // Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Choose Store', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: kText)), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration(color: Colors.grey[100], shape: BoxShape.circle), + child: const Icon(Icons.close, size: 20, color: Colors.black54), + ), + ), + ], + ), + ), + // Search + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: TextField( + controller: _searchCtrl, + onChanged: _filter, + decoration: InputDecoration( + hintText: 'Search stores...', + hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + filled: true, + fillColor: const Color(0xFFF5F5F5), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + // Store list + ConstrainedBox( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.45), + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _filtered.length, + itemBuilder: (context, i) { + final store = _filtered[i]; + final isSel = store.name == _selected.name; + return GestureDetector( + onTap: () => setState(() => _selected = store), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + // color: isSel ? kGreenLight : Colors.white, + color: Colors.white, + borderRadius: BorderRadius.circular(14), + // border: Border.all(color: isSel ? kGreen : const Color(0xFFE0E0E0), width: isSel ? 2 : 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 3, + offset: const Offset(0, 1), + ), + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 6), + ), + ], + ), + child: Row( + children: [ + Container( + width: 50, height: 50, + decoration: BoxDecoration(color: store.color, borderRadius: BorderRadius.circular(12)), + child: const Icon(Icons.shopping_basket, color: Colors.white, size: 26), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(store.name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: kText)), + const SizedBox(height: 4), + Row( + children: [ + const Icon(Icons.star, color: Color(0xFFFFB300), size: 14), + const SizedBox(width: 3), + Text('${store.rating}', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: kText)), + const SizedBox(width: 6), + Container(width: 3, height: 3, + decoration: BoxDecoration(color: Colors.grey[400], shape: BoxShape.circle)), + const SizedBox(width: 6), + Text(store.deliveryTime, + style: TextStyle(fontSize: 13, color: Colors.grey[600])), + ], + ), + Row( + children: [ + Icon(Icons.location_on, size: 13, color: Colors.grey[500]), + const SizedBox(width: 2), + Text(store.distance, + style: TextStyle(fontSize: 12, color: Colors.grey[500])), + ], + ), + ], + ), + ), + Container( + width: 22, height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: isSel ? kGreen : Colors.grey[400]!, width: 2), + ), + child: isSel + ? Center( + child: Container( + width: 12, height: 12, + decoration: const BoxDecoration(color: kGreen, shape: BoxShape.circle), + ), + ) + : null, + ), + ], + ), + ), + ); + }, + ), + ), + // Confirm button + Padding( + padding: EdgeInsets.fromLTRB(16, 8, 16, MediaQuery.of(context).padding.bottom + 16), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + widget.onStoreSelected(_selected); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + elevation: 0, + ), + child: const Text('Confirm Store', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ), + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────── +// SHEET — ADD PRODUCTS +// ───────────────────────────────────────── + +class AddProductsSheet extends StatefulWidget { + final List selectedProducts; + final Function(List) onProductsAdded; + + const AddProductsSheet({super.key, required this.selectedProducts, required this.onProductsAdded}); + + @override + State createState() => _AddProductsSheetState(); +} + +class _AddProductsSheetState extends State { + String _selectedCategory = 'Milk & Dairy'; + final TextEditingController _searchCtrl = TextEditingController(); + late Map> _allProducts; + late List _selected; + String _searchQuery = ''; + + final List> _categories = [ + {'name': 'Milk & Dairy', 'emoji': '🥛'}, + {'name': 'Vegetables', 'emoji': '🥦'}, + {'name': 'Fruits', 'emoji': '🍎'}, + {'name': 'Staples', 'emoji': '🌾'}, + {'name': 'Snacks', 'emoji': '🍪'}, + {'name': 'Beverages', 'emoji': '🍵'}, + {'name': 'Bakery', 'emoji': '🍞'}, + ]; + + @override + void initState() { + super.initState(); + _allProducts = getAllProducts(); + _selected = List.from(widget.selectedProducts); + for (var catList in _allProducts.values) { + for (var p in catList) { + if (_selected.any((sp) => sp.id == p.id)) p.isSelected = true; + } + } + } + + List get _displayed { + final products = _allProducts[_selectedCategory] ?? []; + if (_searchQuery.isEmpty) return products; + return products.where((p) => p.name.toLowerCase().contains(_searchQuery.toLowerCase())).toList(); + } + + void _toggle(Product p) { + setState(() { + p.isSelected = !p.isSelected; + if (p.isSelected) { _selected.add(p); } else { _selected.removeWhere((s) => s.id == p.id); } + }); + } + + double get _total => _selected.fold(0.0, (s, p) => s + p.price); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.92, + minChildSize: 0.6, + maxChildSize: 0.95, + expand: false, + builder: (context, scroll) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Handle + Container( + margin: const EdgeInsets.only(top: 10), + width: 40, height: 4, + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), + ), + // Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 0), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.close, size: 24, color: kText), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Add products from Fresh Mart', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: kText)), + Text('Choose the items you need regularly', + style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 12), + // Search & Filter + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchCtrl, + onChanged: (q) => setState(() => _searchQuery = q), + decoration: InputDecoration( + hintText: 'Search products...', + hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), + prefixIcon: const Icon(Icons.search, color: Colors.grey), + filled: true, + fillColor: const Color(0xFFF5F5F5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE0E0E0)), + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + children: [ + Icon(Icons.tune, color: kGreen, size: 18), + SizedBox(width: 6), + Text('Filter', style: TextStyle(color: kGreen, fontWeight: FontWeight.w600, fontSize: 14)), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 14), + // Category row + SizedBox( + height: 86, + + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _categories.length, + itemBuilder: (context, i) { + final cat = _categories[i]; + final isSel = cat['name'] == _selectedCategory; + return GestureDetector( + onTap: () => setState(() => _selectedCategory = cat['name']), + child: Container( + margin: const EdgeInsets.only(right: 14), + child: Column( + children: [ + Container( + width: 56, height: 56, + decoration: BoxDecoration( + color: isSel ? kGreenLight : const Color(0xFFF5F5F5), + shape: BoxShape.circle, + border: isSel ? Border.all(color: kGreen, width: 2) : null, + ), + child: Center(child: Text(cat['emoji'], style: const TextStyle(fontSize: 26))), + ), + const SizedBox(height: 4), + Text(cat['name'], + style: TextStyle( + fontSize: 10, + fontWeight: isSel ? FontWeight.bold : FontWeight.normal, + color: isSel ? kGreen : Colors.grey[700], + )), + ], + ), + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider(color: Colors.grey[200], height: 1), + ), + // Category title & sort + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('$_selectedCategory (${_allProducts[_selectedCategory]?.length ?? 0})', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: kText)), + const Row( + children: [ + Text('Sort', style: TextStyle(color: kGreen, fontWeight: FontWeight.w600, fontSize: 14)), + SizedBox(width: 4), + Icon(Icons.sort, color: kGreen, size: 18), + ], + ), + ], + ), + ), + // Grid + Expanded( + child: GridView.builder( + controller: scroll, + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.76, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: _displayed.length + 1, + itemBuilder: (context, i) { + if (i == _displayed.length) return _browseAllCard(); + return _productCard(_displayed[i]); + }, + ), + ), + // Bottom selected bar + if (_selected.isNotEmpty) _selectedBar(context), + ], + ), + ); + }, + ); + } + + Widget _productCard(Product p) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + border: Border.all(color: p.isSelected ? kGreen : const Color(0xFFE8E8E8), width: p.isSelected ? 2 : 1), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Center(child: Text(p.emoji, style: const TextStyle(fontSize: 50)))), + if (p.isPopular) + Container( + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration(color: kGreenLight, borderRadius: BorderRadius.circular(6)), + child: const Text('Popular', style: TextStyle(fontSize: 11, color: kGreen, fontWeight: FontWeight.w600)), + ), + Text(p.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: kText)), + Text(p.quantity, style: TextStyle(fontSize: 12, color: Colors.grey[500])), + const SizedBox(height: 4), + Text('₹${p.price.toStringAsFixed(0)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: kText)), + ], + ), + ), + Positioned( + bottom: 8, right: 8, + child: GestureDetector( + onTap: () => _toggle(p), + child: Container( + width: 28, height: 28, + decoration: BoxDecoration( + color: p.isSelected ? kGreen : Colors.white, + shape: BoxShape.circle, + border: Border.all(color: p.isSelected ? kGreen : const Color(0xFFD0D0D0), width: 1.5), + ), + child: Icon(p.isSelected ? Icons.check : Icons.add, + size: 16, color: p.isSelected ? Colors.white : kGreen), + ), + ), + ), + ], + ), + ); + } + + Widget _browseAllCard() { + return Container( + decoration: BoxDecoration( + color: const Color(0xFFF9F9F9), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE8E8E8)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.shopping_basket_outlined, color: kGreen, size: 36), + const SizedBox(height: 8), + const Text("Can't find what\nyou're looking for?", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: kText, fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + Text('Browse all 340+ products from Fresh Mart', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 10, color: Colors.grey[500])), + const SizedBox(height: 8), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Browse all', style: TextStyle(color: kGreen, fontWeight: FontWeight.bold, fontSize: 13)), + Icon(Icons.chevron_right, color: kGreen, size: 18), + ], + ), + ], + ), + ); + } + + Widget _selectedBar(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, -4))], + ), + child: Column( + children: [ + SizedBox( + height: 54, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + children: _selected.map((p) => Container( + margin: const EdgeInsets.only(right: 8), + child: Stack( + children: [ + Container( + width: 38, height: 38, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE0E0E0)), + ), + child: Center(child: Text(p.emoji, style: const TextStyle(fontSize: 20))), + ), + Positioned( + top: -2, right: -2, + child: GestureDetector( + onTap: () => _toggle(p), + child: Container( + width: 16, height: 16, + decoration: BoxDecoration(color: Colors.grey[400], shape: BoxShape.circle), + child: const Icon(Icons.close, size: 10, color: Colors.white), + ), + ), + ), + ], + ), + )).toList(), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(16, 4, 16, MediaQuery.of(context).padding.bottom + 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${_selected.length} items selected', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: kText)), + GestureDetector( + onTap: () => setState(() { + for (var catList in _allProducts.values) { + for (var p in catList) p.isSelected = false; + } + _selected.clear(); + }), + child: const Text('Clear all', + style: TextStyle(color: kGreen, fontSize: 13, fontWeight: FontWeight.w500)), + ), + ], + ), + ElevatedButton( + onPressed: () { + widget.onProductsAdded(_selected); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: kGreen, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: Row( + children: [ + Text('Add ${_selected.length} items ₹${_total.toStringAsFixed(0)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), + const SizedBox(width: 4), + const Icon(Icons.chevron_right, size: 18), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/dash_view.dart b/lib/features/dashboard/presentation/screens/dash_view.dart new file mode 100644 index 0000000..5c4448f --- /dev/null +++ b/lib/features/dashboard/presentation/screens/dash_view.dart @@ -0,0 +1,1551 @@ +import 'dart:async'; + +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_instance/src/extension_instance.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:nearledaily/constants/color_constants.dart'; +import 'package:nearledaily/features/dashboard/presentation/screens/voice_assistant_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../../constants/font_constants.dart'; +import '../../../../controllers/cart_controller/cart.dart'; +import '../../../../controllers/dashboard_controller/dashboard_controller.dart'; +import '../../../../domain/repository/authentication/auth_repository.dart'; +import '../../../../modules/search_model/search_model.dart'; +import '../../../../view/dashboard_view/searchScreen.dart'; +import '../../../../view/home_view.dart'; +import '../../../../view/product/product_details.dart'; +import '../../../../view/product/products_list.dart' hide ProductItem; +import '../../../../widgets/text_widget.dart'; +import '../../../Products/product_card.dart'; +import 'ai_processing_screen.dart'; +import 'camera_search.dart'; +import 'daily_plan.dart' hide Product; +import '../../../../view/product/products_list.dart' as pl; + + +import '../../domain/repo/grocery_repository.dart'; + +// TODO: same path as in grocery_repository.dart — must point at the same +// `Product` class used by CartController.addToCart. +import '../../../../modules/product/product.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +final products = [ + // ── Food Delivery ── + ProductItem( + name: 'Chicken Biryani', + category: 'Food Delivery · HotPot', + price: '₹189', + originalPrice: '₹240', + image: 'https://images.unsplash.com/photo-1563379091339-03b21ab4a4f8?w=300&fit=crop', + rating: 4.8, + reviewCount: 320, + ), + ProductItem( + name: 'Margherita Pizza', + category: 'Food Delivery · Dominos', + price: '₹149', + originalPrice: '₹199', + image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=300&fit=crop', + rating: 4.6, + reviewCount: 512, + ), + ProductItem( + name: 'Paneer Butter Masala', + category: 'Food Delivery · Behrouz', + price: '₹220', + originalPrice: '₹280', + image: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc7?w=300&fit=crop', + rating: 4.7, + reviewCount: 210, + ), + ProductItem( + name: 'Mango Smoothie', + category: 'Food Delivery · Juice Bar', + price: '₹89', + originalPrice: '₹120', + image: 'https://images.unsplash.com/photo-1623065422902-30a2d299bbe4?w=300&fit=crop', + rating: 4.4, + reviewCount: 67, + ), + + // ── Grocery · Vegetables ── + ProductItem( + name: 'Fresh Veggie Basket', + category: 'Grocery · Vegetables', + price: '₹299', + originalPrice: '₹420', + image: 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=300&fit=crop', + rating: 4.5, + reviewCount: 180, + ), + ProductItem( + name: 'Fresh Tomatoes 1kg', + category: 'Grocery · Vegetables', + price: '₹40', + originalPrice: '₹50', + image: 'https://images.unsplash.com/photo-1546470427-e26264be0b0d?w=300&fit=crop', + rating: 4.3, + reviewCount: 96, + ), + ProductItem( + name: 'Onion 1kg', + category: 'Grocery · Vegetables', + price: '₹35', + originalPrice: '₹45', + image: 'https://images.unsplash.com/photo-1580201092675-a0a6a6cafbb1?w=300&fit=crop', + rating: 4.2, + reviewCount: 74, + ), + ProductItem( + name: 'Green Capsicum 500g', + category: 'Grocery · Vegetables', + price: '₹30', + originalPrice: '₹40', + image: 'https://images.unsplash.com/photo-1563565375-f3fdfdbefa83?w=300&fit=crop', + rating: 4.1, + reviewCount: 41, + ), + + // ── Grocery · Fruits ── + ProductItem( + name: 'Fresh Apples 1kg', + category: 'Grocery · Fruits', + price: '₹180', + originalPrice: '₹220', + image: 'https://images.unsplash.com/photo-1560806887-1e4cd0b6cbd6?w=300&fit=crop', + rating: 4.6, + reviewCount: 130, + ), + ProductItem( + name: 'Banana Dozen', + category: 'Grocery · Fruits', + price: '₹50', + originalPrice: '₹60', + image: 'https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?w=300&fit=crop', + rating: 4.5, + reviewCount: 210, + ), + ProductItem( + name: 'Alphonso Mango Box', + category: 'Grocery · Fruits', + price: '₹399', + originalPrice: '₹499', + image: 'https://images.unsplash.com/photo-1591073113125-e46713c829ed?w=300&fit=crop', + rating: 4.8, + reviewCount: 88, + ), + ProductItem( + name: 'Pomegranate 1kg', + category: 'Grocery · Fruits', + price: '₹120', + originalPrice: '₹150', + image: 'https://images.unsplash.com/photo-1541344999736-83eca272f6fc?w=300&fit=crop', + rating: 4.4, + reviewCount: 57, + ), + + // ── Grocery · Dairy & Eggs ── + ProductItem( + name: 'Toned Milk 1L', + category: 'Grocery · Dairy & Eggs', + price: '₹58', + originalPrice: '₹64', + image: 'https://images.unsplash.com/photo-1550583724-b2692b85b150?w=300&fit=crop', + rating: 4.7, + reviewCount: 300, + ), + ProductItem( + name: 'Farm Fresh Eggs (12 pcs)', + category: 'Grocery · Dairy & Eggs', + price: '₹84', + originalPrice: '₹96', + image: 'https://images.unsplash.com/photo-1518569656558-1f25e69d93d7?w=300&fit=crop', + rating: 4.6, + reviewCount: 145, + ), + ProductItem( + name: 'Paneer 200g', + category: 'Grocery · Dairy & Eggs', + price: '₹90', + originalPrice: '₹110', + image: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc7?w=300&fit=crop', + rating: 4.5, + reviewCount: 92, + ), + ProductItem( + name: 'Fresh Curd 400g', + category: 'Grocery · Dairy & Eggs', + price: '₹45', + originalPrice: '₹55', + image: 'https://images.unsplash.com/photo-1571212515416-fca988083b28?w=300&fit=crop', + rating: 4.4, + reviewCount: 60, + ), + + // ── Grocery · Staples & Grains ── + ProductItem( + name: 'Gold Winner Sunflower Refined Oil', + category: 'Grocery · Staples & Grains', + price: '₹89', + originalPrice: '₹111', + image: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?w=300&fit=crop', + rating: 4.2, + reviewCount: 95, + ), + ProductItem( + name: 'Basmati Rice 1kg', + category: 'Grocery · Staples & Grains', + price: '₹120', + originalPrice: '₹150', + image: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?w=300&fit=crop', + rating: 4.5, + reviewCount: 110, + ), + ProductItem( + name: 'Whole Wheat Atta 5kg', + category: 'Grocery · Staples & Grains', + price: '₹249', + originalPrice: '₹289', + image: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=300&fit=crop', + rating: 4.6, + reviewCount: 132, + ), + + // ── Grocery · Snacks & Beverages ── + ProductItem( + name: 'Kurkure Namkeen Masala Munch', + category: 'Grocery · Snacks & Beverages', + price: '₹54', + originalPrice: '₹60', + image: 'https://images.unsplash.com/photo-1600952841320-db92ec4047ca?w=300&fit=crop', + rating: 4.3, + reviewCount: 210, + ), + ProductItem( + name: 'Cola 750ml', + category: 'Grocery · Snacks & Beverages', + price: '₹40', + originalPrice: '₹45', + image: 'https://images.unsplash.com/photo-1554866585-cd94860890b7?w=300&fit=crop', + rating: 4.1, + reviewCount: 88, + ), + ProductItem( + name: 'Classic Potato Chips', + category: 'Grocery · Snacks & Beverages', + price: '₹20', + originalPrice: '₹25', + image: 'https://images.unsplash.com/photo-1566478989037-eec170784d0b?w=300&fit=crop', + rating: 4.2, + reviewCount: 76, + ), + + // ── Wellness ── + ProductItem( + name: 'Multivitamin Tablets', + category: 'Wellness · HealthKart', + price: '₹399', + originalPrice: '₹549', + image: 'https://images.unsplash.com/photo-1584017911766-d451b3d0e843?w=300&fit=crop', + rating: 4.6, + reviewCount: 88, + ), + ProductItem( + name: 'Yoga Mat Premium', + category: 'Wellness · FitZone', + price: '₹699', + originalPrice: '₹999', + image: 'https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=300&fit=crop', + rating: 4.5, + reviewCount: 54, + ), + ProductItem( + name: 'Full Body Checkup', + category: 'Wellness · MediCare', + price: '₹1499', + originalPrice: '₹1999', + image: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=300&fit=crop', + rating: 4.7, + reviewCount: 210, + ), + + // ── Electronics ── + ProductItem( + name: 'Wireless Earbuds', + category: 'Electronics · SoundWave', + price: '₹1299', + originalPrice: '₹1999', + image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=300&fit=crop', + rating: 4.5, + reviewCount: 340, + ), + ProductItem( + name: 'Smart Watch', + category: 'Electronics · TimeTech', + price: '₹2199', + originalPrice: '₹2999', + image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=300&fit=crop', + rating: 4.6, + reviewCount: 198, + ), + ProductItem( + name: 'Bluetooth Speaker', + category: 'Electronics · SoundWave', + price: '₹999', + originalPrice: '₹1499', + image: 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=300&fit=crop', + rating: 4.4, + reviewCount: 150, + ), + + // ── Offers ── + ProductItem( + name: 'Combo Pack Snacks', + category: 'Offers · Deal Zone', + price: '₹199', + originalPrice: '₹349', + image: 'https://images.unsplash.com/photo-1621939514649-280e2ee25f60?w=300&fit=crop', + rating: 4.2, + reviewCount: 60, + ), + ProductItem( + name: 'Buy 1 Get 1 Sunflower Oil', + category: 'Offers · Grocery Deals', + price: '₹99', + originalPrice: '₹198', + image: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?w=300&fit=crop', + rating: 4.3, + reviewCount: 75, + ), + ProductItem( + name: 'Flat 40% Off Earbuds', + category: 'Offers · Electronics Deals', + price: '₹799', + originalPrice: '₹1299', + image: 'https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=300&fit=crop', + rating: 4.4, + reviewCount: 220, + ), +]; + +class _HomeScreenState extends State + with SingleTickerProviderStateMixin { + final DashboardController controller = Get.put(DashboardController()); + + // ─── CART ──────────────────────────────────────────────────────────────── + final CartController cartController = Get.put(CartController()); + // Set this to true (e.g. from a scroll listener) to slide the cart bar + // down and fade it out. Left false = always visible while cart has items. + bool _hideCartBar = false; + + // ─── GROCERY (fetched live) ───────────────────────────────────────────── + final GroceryRepository _groceryRepository = GroceryRepository(); + List _grocerySections = []; + bool _isLoadingGrocery = false; + String? _groceryError; + + // TODO: source these from wherever the app tracks the selected + // store/tenant and delivery location instead of hardcoding them. + static const int _groceryCategoryId = 2; + static const int _tenantId = 1087; + static const int _locationId = 1135; + + + Future _fetchGroceryProducts() async { + setState(() { + _isLoadingGrocery = true; + _groceryError = null; + }); + try { + final sections = await _groceryRepository.fetchGroceryProducts( + categoryId: _groceryCategoryId, + tenantId: _tenantId, + locationId: _locationId, + ); + if (!mounted) return; + setState(() { + _grocerySections = sections; + _isLoadingGrocery = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _groceryError = e.toString().replaceFirst('Exception: ', ''); + _isLoadingGrocery = false; + }); + } + } + + String Name = ''; + String Adress = ''; + String Profile = ''; + + // Currently active top-bar tab. Defaults to the first tab. + String _selectedCategory = 'Grocery'; + + // ─── SHIMMER-ON-TAB-SWITCH STATE ─────────────────────────────────────── + bool _isTabSwitching = false; + Timer? _shimmerTimer; + late final AnimationController _shimmerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); + + static const _kName = 'cached_name'; + static const _kAddress = 'cached_address'; + static const _kProfile = 'cached_profile'; + + Future _loadProfile() async { + final prefs = await SharedPreferences.getInstance(); + final int? id = prefs.getInt('customerId'); + + if (id == null) { + Get.snackbar("Error", "Customer ID not found"); + return; + } + + final cachedName = prefs.getString(_kName) ?? ''; + final cachedAddress = prefs.getString(_kAddress) ?? ''; + final cachedProfile = prefs.getString(_kProfile) ?? ''; + + if (cachedName.isNotEmpty || cachedAddress.isNotEmpty) { + setState(() { + Name = cachedName; + Adress = cachedAddress; + Profile = cachedProfile; + }); + } + + final repo = LoginRepository(); + final fetchedProfile = await repo.fetchProfile(id.toString()); + + if (fetchedProfile != null) { + final newName = fetchedProfile.firstname ?? ''; + final newProfile = fetchedProfile.profileimage ?? ''; + final newAddress = fetchedProfile.address ?? ''; + + if (Name != newName || Profile != newProfile || Adress != newAddress) { + setState(() { + Name = newName; + Profile = newProfile; + Adress = newAddress; + }); + + await prefs.setString(_kName, newName); + await prefs.setString(_kAddress, newAddress); + await prefs.setString(_kProfile, newProfile); + } else { + print("Profile unchanged, no UI update"); + } + } + } + + @override + void initState() { + super.initState(); + + _loadProfile(); + _fetchGroceryProducts(); + } + + @override + void dispose() { + _shimmerTimer?.cancel(); + _shimmerController.dispose(); + super.dispose(); + } + + String getGreeting() { + final hour = DateTime.now().hour; + if (hour < 12) return "Good Morning"; + else if (hour < 17) return "Good Afternoon"; + else return "Good Evening"; + } + + // ─── TAB SWITCH → TRIGGER 2s FULL-BODY SHIMMER ───────────────────────── + void _onTopTabSelected(String title) { + if (title == _selectedCategory) return; + setState(() { + _selectedCategory = title; + _isTabSwitching = true; + }); + _shimmerTimer?.cancel(); + _shimmerTimer = Timer(const Duration(seconds: 2), () { + if (mounted) setState(() => _isTabSwitching = false); + }); + } + + // ─── FILTERING LOGIC (non-Grocery tabs) ──────────────────────────────── + // Filters `products` whose `category` field contains the selected tab's + // name (case-insensitive), e.g. "Wellness" matches "Wellness · HealthKart". + List get _filteredProducts { + final keyword = _selectedCategory.toLowerCase(); + return products + .where((p) => p.category.toLowerCase().contains(keyword)) + .toList(); + } + + // ─── GROCERY SECTIONS (fetched live), REORDERED SO THE TAPPED ────────── + // quick-category is first — e.g. tapping "Vegetables & Fruits" in Shop-by- + // Category moves that row to the top; everything else keeps its order. + List get _orderedGrocerySections { + final sections = + _grocerySections.where((s) => s.products.isNotEmpty).toList(); + + final selected = _selectedGrocerySubcategory; + if (selected != null) { + sections.sort((a, b) { + final aMatch = a.subcategoryName == selected; + final bMatch = b.subcategoryName == selected; + if (aMatch == bMatch) return 0; + return aMatch ? -1 : 1; + }); + } + return sections; + } + + @override + Widget build(BuildContext context) { + final isGrocery = _selectedCategory == 'Grocery'; + final filtered = _filteredProducts; + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Stack( + children: [ + Column( + children: [ + // ── Top bar + search bar are ALWAYS real, never shimmered ── + _buildTopBar(), + _buildSearchBar(), + _buildExploreMore(), + Expanded( + child: SingleChildScrollView( + child: _isTabSwitching + ? _buildShimmerSkeleton() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + + // ── Banner + quick category strip: Grocery tab only ── + if (isGrocery) ...[ + if (_orderedGrocerySections.isNotEmpty) ...[ + _banner(), + const SizedBox(height: 10), + ], + + _buildShopByCategory(), + ], + + if (isGrocery) ...[ + // ── Grocery: loading / error / data states ── + if (_isLoadingGrocery) + const Padding( + padding: EdgeInsets.symmetric(vertical: 60), + child: Center(child: CircularProgressIndicator()), + ) + else if (_groceryError != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 40), + child: Center( + child: Column( + children: [ + Icon(Icons.wifi_off_rounded, + size: 36, color: Colors.grey.shade400), + const SizedBox(height: 8), + Text( + _groceryError!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 13), + ), + const SizedBox(height: 12), + OutlinedButton( + onPressed: _fetchGroceryProducts, + child: const Text('Retry'), + ), + ], + ), + ), + ) + else if (_orderedGrocerySections.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + 'https://cdn-icons-png.flaticon.com/512/6134/6134065.png', + height: 140, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.location_off_rounded, + size: 90, + color: Colors.grey, + ); + }, + ), + const SizedBox(height: 20), + const Text( + "We're not delivering here yet", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + "We're expanding to new locations. Stay tuned—we'll be serving your area soon!", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + height: 1.5, + ), + ), + ], + ), + ), + ) + else + ..._orderedGrocerySections.map((s) => + _buildGrocerySection( + s.subcategoryName, s.products)), + ] else ...[ + // ── Other tabs: single header + grid ── + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 12), + child: Text( + _selectedCategory, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 8), + if (filtered.isEmpty) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 40), + child: Center( + child: Column( + children: [ + Icon(Icons.inventory_2_outlined, + size: 40, color: Colors.grey.shade400), + const SizedBox(height: 8), + Text( + 'No items in this category yet', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 13), + ), + ], + ), + ), + ) + else + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 24), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + mainAxisExtent: 270, + ), + itemCount: filtered.length, + itemBuilder: (context, index) { + final item = filtered[index]; + return GestureDetector( + onTap: () => _openProduct(context, item), + child: ProductCard(item: item), + ); + }, + ), + ), + ], + + // const SizedBox(height: 16), + // _buildCreatedDailyPlans(), + const SizedBox(height: 16), + // Extra bottom padding so content doesn't sit behind + // the floating cart bar. + const SizedBox(height: 90), + Image.asset( + 'assets/images/nearle_copyrights.png'), + ], + ), + ), + ), + ], + ), + + // ── Floating "view cart" bar, overlaid above everything ── + _buildFloatingCartBar(), + ], + ), + ), + ); + } + + // ─── SHARED: open product sheet ───────────────────────────────────────── + void _openProduct(BuildContext context, ProductItem item) { + showProductSheet( + context, + pl.ProductItem( + + name: item.name, + category: item.category, + price: item.price, + originalPrice: item.originalPrice, + imageUrl: item.image, + bgColor: const Color(0xFFFCE4EC), + rating: item.rating, + reviewCount: item.reviewCount, + tag: item.category.split('·').first.trim(), + + ), + ); + } + + // ─── GROCERY SUBCATEGORY SECTION (Vegetables / Fruits / Dairy & Eggs…) ── + Widget _buildGrocerySection(String title, List items) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionHeader(title), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + mainAxisExtent: 230, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final product = items[index]; + return Obx(() { + final cartItem = _findCartItem(product); + final qty = cartItem?.quantity ?? 0; + + return GestureDetector( + onTap: () => _openGroceryProduct(context, product), + child: ProductCard( + item: _groceryDisplayItem(product), + quantity: qty, + onAddTap: () => _addGroceryToCart(product), + onIncrementTap: () { + if (cartItem != null) cartController.increaseQty(cartItem); + }, + onDecrementTap: () { + if (cartItem != null) cartController.decreaseQty(cartItem); + }, + ), + ); + }); + }, + ), + ), + const SizedBox(height: 16), + ], + ); + } + + + ProductItem _groceryDisplayItem(Product p) { + final num cost = p.productcost ?? 0; + final num discount = p.discount ?? 0; + final num original = discount > 0 ? cost + discount : cost; + + return ProductItem( + name: p.productname ?? 'Product', + category: 'Grocery · ${p.subcategoryname ?? ''}', + price: _formatRupees(cost), + originalPrice: _formatRupees(original), + image: (p.productimage != null && p.productimage!.isNotEmpty) + ? p.productimage! + : 'https://via.placeholder.com/300?text=No+Image', + rating: 4.0, + reviewCount: 0, + ); + } + + String _formatRupees(num value) { + final isWhole = value == value.roundToDouble(); + return '₹${isWhole ? value.toStringAsFixed(0) : value.toStringAsFixed(2)}'; + } + + // ─── GROCERY: ADD TO CART ──────────────────────────────────────────────── + void _addGroceryToCart(Product product) { + cartController.addToCart( + product, + qty: 1, + locationId: _locationId.toString(), + ); + } + + void _openGroceryProduct(BuildContext context, Product product) { + final display = _groceryDisplayItem(product); + showProductSheet( + context, + pl.ProductItem( + name: display.name, + category: display.category, + price: display.price, + originalPrice: display.originalPrice, + imageUrl: display.image, + bgColor: const Color(0xFFFCE4EC), + rating: display.rating, + reviewCount: display.reviewCount, + tag: display.category.split('·').first.trim(), + ), + ); + } + + // ─── TOP BAR ─────────────────────────────────────────────────────────────── + Widget _buildTopBar() { + return Container( + color: const Color(0xFFDCE3F7), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + const SizedBox(width: 4), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: "${getGreeting()}, ${Name.isNotEmpty ? Name : 'User'}", + color: Colors.black87, + fontFamily: FontConstants.fontFamily, + fontSize: 15, + fontWeight: FontWeight.w600, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + GestureDetector( + onTap: () { + controller.location(); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ReusableTextWidget( + text: (Adress != null && Adress.trim().isNotEmpty) + ? (Adress.length > 25 + ? "${Adress.substring(0, 25)}..." + : Adress) + : "No Address", + color: Colors.grey[700]!, + fontFamily: FontConstants.fontFamily, + fontSize: 13, + fontWeight: FontWeight.w500, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 2), + const Icon( + Icons.keyboard_arrow_down, + size: 18, + color: Colors.grey, + ), + ], + ), + ), + ], + ), + const Spacer(), + const SizedBox(width: 12), + GestureDetector( + + onTap: () { + final navController = Get.find(); + navController.currentIndex.value = 4; + }, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.10), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: CircleAvatar( + backgroundColor: Colors.white, + backgroundImage: Profile.isNotEmpty + ? NetworkImage(Profile) + : null, + child: Profile.isEmpty + ? const Icon( + Icons.person, + color: Colors.black54, + size: 22, + ) + : null, + ), + ), + ), + + const SizedBox(width: 8), + ], + ), + ); + } + + // ─── SEARCH BAR ──────────────────────────────────────────────────────────── + Widget _buildSearchBar() { + return Container( + color: const Color(0xFFDCE3F7), + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const SizedBox(width: 12), + const Icon(Icons.search, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: () { + Get.to(() => const SearchScreen()); + }, + child: AbsorbPointer( + absorbing: true, + child: TextField( + decoration: const InputDecoration( + hintText: 'Search "milk", "eggs", "dosa", "plumber"...', + hintStyle: TextStyle(color: Colors.grey, fontSize: 13), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ), + ), + IconButton(onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const VoiceAssistantScreen()), + ); + }, icon: const Icon(Icons.mic_none, color: Colors.grey)), + const SizedBox(width: 12), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LensSearchScreen()), + ); + }, + child: const Icon(Icons.camera_alt_outlined, color: Colors.grey), + ), + const SizedBox(width: 12), + ], + ), + ), + ); + } + + // ─── EXPLORE MORE / TOP CATEGORY TABS (Blinkit-style) ────────────────────── + Widget _buildExploreMore() { + final items = [ + {'title': 'Grocery', 'icon': Icons.shopping_basket_outlined}, + {'title': 'Food Delivery', 'icon': Icons.delivery_dining}, + {'title': 'Wellness', 'icon': Icons.favorite_border}, + {'title': 'Electronics', 'icon': Icons.headphones}, + {'title': 'Offers', 'icon': Icons.local_offer_outlined}, + ]; + + return LayoutBuilder( + builder: (context, constraints) { + final screenWidth = MediaQuery.of(context).size.width; + + final scale = (screenWidth / 375).clamp(0.8, 1.15); + + final iconSize = 22.0 * scale; // was 26 + final fontSize = 10.5 * scale; // was 12 + final horizontalPadding = 14.0 * scale; // was 16 + final itemSpacing = 18.0 * scale; // was 22 + final containerHeight = 62.0 * scale; // was 72 + final underlineWidth = 22.0 * scale; // was 28 + + return Container( + color: const Color(0xFFDCE3F7), + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + height: containerHeight, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + itemCount: items.length, + separatorBuilder: (_, __) => SizedBox(width: itemSpacing), + itemBuilder: (_, i) { + final item = items[i]; + final title = item['title'] as String; + final isSelected = _selectedCategory == title; + final color = isSelected + ? const Color(0xFF1A2B6D) + : const Color(0xFF5B6BAE); + + return GestureDetector( + onTap: () => _onTopTabSelected(title), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(item['icon'] as IconData, size: iconSize, color: color), + SizedBox(height: 4 * scale), + Text( + title, + style: TextStyle( + fontSize: fontSize, + fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, + color: color, + ), + ), + SizedBox(height: 4 * scale), + Container( + height: 2 * scale, + width: underlineWidth, + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF1A2B6D) : Colors.transparent, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ); + }, + ), + ), + ); + }, + ); + } + // ─── SHOP BY CATEGORY (quick jump icons — Grocery tab only) ──────────────── + // Built from the live subcategories returned by the API (uses each + // section's own `image`), rather than a hardcoded icon set. + Widget _buildShopByCategory() { + final sections = + _grocerySections.where((s) => s.products.isNotEmpty).toList(); + + if (sections.isEmpty) return const SizedBox(); + + return Container( + decoration: BoxDecoration( + + border: Border( + top: BorderSide(color: Colors.grey.shade300, width: 0.5), + bottom: BorderSide(color: Colors.grey.shade300, width: 0.5), + ), + ), + child: Column( + children: [ + _sectionHeader('Category'), + const SizedBox(height: 8), + SizedBox( + height: 90, + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + itemCount: sections.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (_, index) { + final section = sections[index]; + final label = section.subcategoryName; + final isSelected = label == _selectedGrocerySubcategory; + + return GestureDetector( + onTap: () => _scrollToGrocerySubcategory(label), + child: SizedBox( + width: 66, + child: Column( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? const Color(0xFFEAF8EC) + : const Color(0xFFF5F5F5), + ), + child: ClipOval( + child: section.image.isNotEmpty + ? Image.network( + section.image, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const Icon( + Icons.shopping_basket_outlined, + size: 20), + ) + : const Icon(Icons.shopping_basket_outlined, + size: 20), + ), + ), + const SizedBox(height: 4), + Text( + label, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 9, + height: 1.15, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? ColorConstants.ratingColor + : Colors.black87, + ), + ), + const Spacer(), + if (isSelected) + Container( + width: 22, + height: 2, + decoration: BoxDecoration( + color: ColorConstants.ratingColor, + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + ), + ); + }, + ), + ) + ], + ), + ); + } + + // Highlight state for the quick-jump icon row above. Selecting a + // subcategory here also reorders `_orderedGrocerySections` so that + // section's row renders first. + String? _selectedGrocerySubcategory; + + void _scrollToGrocerySubcategory(String label) { + setState(() { + // Toggle off if tapping the same one again, restoring default order. + _selectedGrocerySubcategory = + _selectedGrocerySubcategory == label ? null : label; + }); + } + + Widget _banner() { + final List bannerImages = [ + 'https://img.magnific.com/free-vector/gradient-shopping-discount-horizontal-sale-banner_23-2150321996.jpg?semt=ais_hybrid&w=740&q=80', + ]; + + return Column( + children: [ + CarouselSlider( + options: CarouselOptions( + height: 180, + autoPlay: true, + enlargeCenterPage: false, + viewportFraction: 1.0, + ), + items: bannerImages.map((url) { + return GestureDetector( + onTap: () { + // Navigator.push( + // context, + // MaterialPageRoute(builder: (_) => const AIAssistantScreen()), + // ); + }, + child: ClipRRect( + child: Image.network( + url, + height: 180, + width: double.infinity, + fit: BoxFit.cover, + + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const SizedBox.shrink(); + }, + errorBuilder: (context, error, stackTrace) => Container( + height: 180, + decoration: BoxDecoration( + color: Colors.grey.shade200, + ), + child: const Center(child: Icon(Icons.broken_image, size: 40)), + ), + ), + ), + ); + }).toList(), + ), + ], + ); + } + + // ─── CREATED DAILY PLANS ─────────────────────────────────────────────────── + Widget _buildCreatedDailyPlans() { + final plans = [ + {'title': 'Breakfast Essentials', 'items': '14 items', 'next': 'Next delivery: Tomorrow', 'color': const Color(0xFFE8F5E9)}, + {'title': 'Home Essentials', 'items': '12 items', 'next': 'Next delivery: 25 May', 'color': const Color(0xFFE3F2FD)}, + {'title': 'Monthly Staples', 'items': '28 items', 'next': 'Next delivery: 1 Jun', 'color': const Color(0xFFFFF8E1)}, + ]; + + return Column( + children: [ + _sectionHeader('Created Daily Plans'), + SizedBox( + height: 100, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + ...plans.map((p) => Container( + width: 150, + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: p['color'] as Color, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.calendar_month, + size: 16, color: Color(0xFF1DB954)), + const SizedBox(width: 4), + Expanded( + child: Text(p['title'] as String, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 11)), + ) + ], + ), + const SizedBox(height: 4), + Text(p['items'] as String, + style: const TextStyle( + color: Colors.black54, fontSize: 10)), + const Spacer(), + Text(p['next'] as String, + style: const TextStyle( + color: Colors.black54, fontSize: 9)), + ], + ), + )), + GestureDetector( + onTap: () { + Get.to(() => const CreateDailyPlanScreen()); + }, + child: Container( + width: 130, + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: + Border.all(color: Colors.grey.shade300, style: BorderStyle.solid), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add, size: 28, color: Color(0xFF1DB954)), + SizedBox(height: 4), + Text('Create New Plan', + style: TextStyle( + color: Color(0xFF1DB954), + fontWeight: FontWeight.bold, + fontSize: 12)), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } + + // ─── FLOATING CART BAR ────────────────────────────────────────────────── + Widget _buildFloatingCartBar() { + return Obx(() { + if (cartController.cartItems.isEmpty) return const SizedBox(); + final tenant = cartController.currentTenant.value; + final tenantImage1 = tenant?.tenantimage ?? ''; + final tenantName1 = tenant?.tenantname ?? 'Unknown Store'; + + return AnimatedPositioned( + duration: const Duration(milliseconds: 480), + curve: Curves.easeInOut, + left: 0, + right: 0, + bottom: _hideCartBar ? -90 : 16, + child: IgnorePointer( + ignoring: _hideCartBar, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 380), + opacity: _hideCartBar ? 0 : 1, + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF662582), + borderRadius: BorderRadius.circular(50), + boxShadow: [ + BoxShadow( + color: const Color(0xFF662582).withOpacity(0.4), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Store avatar + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.2), + ), + child: ClipOval( + child: tenantImage1.isNotEmpty + ? Image.network(tenantImage1, fit: BoxFit.cover) + : const Icon(Icons.store_rounded, size: 20, color: Colors.white), + ), + ), + + const SizedBox(width: 10), + + // Store name + item count + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 160), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tenantName1, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0.1, + ), + ), + const SizedBox(height: 1), + Text( + "${cartController.totalItems} item${cartController.totalItems > 1 ? 's' : ''}", + style: TextStyle( + fontSize: 11, + color: Colors.white.withOpacity(0.7), + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + + const SizedBox(width: 10), + + // View button — white pill + GestureDetector( + onTap: () { + final navController = Get.find(); + navController.currentIndex.value = 3; + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(40), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + "View", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Color(0xFF662582), + letterSpacing: 0.2, + ), + ), + SizedBox(width: 4), + Icon(Icons.arrow_forward_rounded, size: 14, color: Color(0xFF662582)), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }); + } + + // ─── HELPERS ─────────────────────────────────────────────────────────────── + Widget _sectionHeader(String title) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16)), + + ], + ), + ); + } + + // ─── SHIMMER SKELETON (shown for 2s whenever the top tab changes) ───────── + Widget _shimmerBox({double? width, double? height, double radius = 8}) { + return AnimatedBuilder( + animation: _shimmerController, + builder: (context, _) { + final t = _shimmerController.value; // 0..1 + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + gradient: LinearGradient( + begin: Alignment(-1 + t * 3, 0), + end: Alignment(0 + t * 3, 0), + colors: [ + Colors.grey.shade300, + Colors.grey.shade100, + Colors.grey.shade300, + ], + stops: const [0.1, 0.5, 0.9], + ), + ), + ); + }, + ); + } + + Widget _buildShimmerSkeleton() { + // Note: top bar + search bar are rendered for real above this widget + // (see build()); this skeleton only covers everything below them. + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // top tabs placeholder + + const SizedBox(height: 20), + // banner placeholder + _shimmerBox(height: 180, radius: 14, width: double.infinity), + const SizedBox(height: 20), + // category icons placeholder + Row( + children: List.generate( + 5, + (i) => Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + children: [ + _shimmerBox(width: 46, height: 46, radius: 23), + const SizedBox(height: 6), + _shimmerBox(width: 40, height: 8), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + // section header + product row placeholder + _shimmerBox(width: 120, height: 16), + const SizedBox(height: 12), + SizedBox( + height: 210, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (_, __) => _shimmerBox( + width: 120, + height: 210, + radius: 12, + ), + ), + ), + const SizedBox(height: 20), + _shimmerBox(width: 120, height: 16), + const SizedBox(height: 12), + SizedBox( + height: 210, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (_, __) => _shimmerBox( + width: 120, + height: 210, + radius: 12, + ), + ), + ), + ], + ), + ); + } + + CartItem? _findCartItem(Product product) { + for (final c in cartController.cartItems) { + if (c.product.productid == product.productid) return c; + } + return null; + } +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/edit_object.dart b/lib/features/dashboard/presentation/screens/edit_object.dart new file mode 100644 index 0000000..8d2eb02 --- /dev/null +++ b/lib/features/dashboard/presentation/screens/edit_object.dart @@ -0,0 +1,597 @@ +import 'dart:io'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_mlkit_object_detection/google_mlkit_object_detection.dart'; +import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; +import 'package:image/image.dart' as img; +import 'package:lottie/lottie.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../../core/services/vector_service.dart'; +import 'camera_search.dart'; + +/// Interactive crop-confirmation screen shown after a photo is captured +/// or picked. Mirrors the Google Lens / Amazon "search by image" pattern: +/// 1. The photo is shown with a draggable crop rectangle, pre-filled +/// with an ML Kit object-detection guess once it's ready. +/// 2. The user can drag the four corner handles (or the box itself) to +/// adjust which region gets searched. +/// 3. Tapping the confirm button crops the image, runs on-device OCR +/// on the cropped region to pull out any product text (brand, +/// model, etc.), sends both the image and text to the backend to +/// get vector embeddings, then shows an animated "Finding similar +/// products" sheet before navigating to the results screen. +class CropConfirmScreen extends StatefulWidget { + final String imagePath; + + const CropConfirmScreen({super.key, required this.imagePath}); + + @override + State createState() => _CropConfirmScreenState(); +} + +enum _Corner { topLeft, topRight, bottomLeft, bottomRight } + +class _CropConfirmScreenState extends State { + final ObjectDetector _objectDetector = ObjectDetector( + options: ObjectDetectorOptions( + mode: DetectionMode.single, + classifyObjects: false, + multipleObjects: true, + ), + ); + + final TextRecognizer _textRecognizer = TextRecognizer(); + + // Original decoded image dimensions (pixels). + double? _imgWidth; + double? _imgHeight; + + // Crop rectangle in *original image pixel* coordinates. + Rect? _cropRect; + + bool _isDetecting = true; + bool _isProcessing = false; + + // Updated every build so gesture callbacks can convert screen <-> image + // coordinates using the current layout. + double _scale = 1; + double _offsetX = 0; + double _offsetY = 0; + + @override + void initState() { + super.initState(); + _init(); + } + + @override + void dispose() { + _objectDetector.close(); + _textRecognizer.close(); + super.dispose(); + } + + Future _init() async { + final bytes = await File(widget.imagePath).readAsBytes(); + final decoded = img.decodeImage(bytes); + if (decoded == null || !mounted) return; + + final w = decoded.width.toDouble(); + final h = decoded.height.toDouble(); + + // Fallback: centered rect covering 80% of the image, used until (or + // unless) object detection finds something. + const inset = 0.1; + final fallback = Rect.fromLTRB( + w * inset, + h * inset, + w * (1 - inset), + h * (1 - inset), + ); + + setState(() { + _imgWidth = w; + _imgHeight = h; + _cropRect = fallback; + }); + + try { + final inputImage = InputImage.fromFilePath(widget.imagePath); + final objects = await _objectDetector.processImage(inputImage); + if (!mounted || objects.isEmpty) { + setState(() => _isDetecting = false); + return; + } + objects.sort((a, b) { + final areaA = a.boundingBox.width * a.boundingBox.height; + final areaB = b.boundingBox.width * b.boundingBox.height; + return areaB.compareTo(areaA); + }); + final box = objects.first.boundingBox; + setState(() { + _cropRect = Rect.fromLTRB( + box.left.clamp(0, w), + box.top.clamp(0, h), + box.right.clamp(0, w), + box.bottom.clamp(0, h), + ); + _isDetecting = false; + }); + } catch (_) { + if (mounted) setState(() => _isDetecting = false); + } + } + + double get _minCropSize { + if (_imgWidth == null || _imgHeight == null) return 40; + return math.min(_imgWidth!, _imgHeight!) * 0.12; + } + + void _updateCorner(_Corner corner, Offset screenDelta) { + if (_cropRect == null) return; + final dx = screenDelta.dx / _scale; + final dy = screenDelta.dy / _scale; + final w = _imgWidth!; + final h = _imgHeight!; + final min = _minCropSize; + + Rect r = _cropRect!; + switch (corner) { + case _Corner.topLeft: + final newLeft = (r.left + dx).clamp(0.0, r.right - min); + final newTop = (r.top + dy).clamp(0.0, r.bottom - min); + r = Rect.fromLTRB(newLeft, newTop, r.right, r.bottom); + break; + case _Corner.topRight: + final newRight = (r.right + dx).clamp(r.left + min, w); + final newTop = (r.top + dy).clamp(0.0, r.bottom - min); + r = Rect.fromLTRB(r.left, newTop, newRight, r.bottom); + break; + case _Corner.bottomLeft: + final newLeft = (r.left + dx).clamp(0.0, r.right - min); + final newBottom = (r.bottom + dy).clamp(r.top + min, h); + r = Rect.fromLTRB(newLeft, r.top, r.right, newBottom); + break; + case _Corner.bottomRight: + final newRight = (r.right + dx).clamp(r.left + min, w); + final newBottom = (r.bottom + dy).clamp(r.top + min, h); + r = Rect.fromLTRB(r.left, r.top, newRight, newBottom); + break; + } + setState(() => _cropRect = r); + } + + void _moveRect(Offset screenDelta) { + if (_cropRect == null) return; + final dx = screenDelta.dx / _scale; + final dy = screenDelta.dy / _scale; + final w = _imgWidth!; + final h = _imgHeight!; + Rect r = _cropRect!.shift(Offset(dx, dy)); + + // Clamp so the rect stays fully inside the image bounds. + if (r.left < 0) r = r.shift(Offset(-r.left, 0)); + if (r.top < 0) r = r.shift(Offset(0, -r.top)); + if (r.right > w) r = r.shift(Offset(w - r.right, 0)); + if (r.bottom > h) r = r.shift(Offset(0, h - r.bottom)); + setState(() => _cropRect = r); + } + + Future _confirmCrop() async { + if (_cropRect == null || _isProcessing) return; + setState(() => _isProcessing = true); + + try { + final bytes = await File(widget.imagePath).readAsBytes(); + final original = img.decodeImage(bytes); + if (original == null) throw Exception('Could not decode image'); + + final r = _cropRect!; + final x = r.left.toInt().clamp(0, original.width - 1); + final y = r.top.toInt().clamp(0, original.height - 1); + final cw = r.width.toInt().clamp(1, original.width - x); + final ch = r.height.toInt().clamp(1, original.height - y); + + final cropped = img.copyCrop(original, x: x, y: y, width: cw, height: ch); + final croppedBytes = img.encodeJpg(cropped, quality: 92); + + final dir = await getTemporaryDirectory(); + final croppedPath = + '${dir.path}/cropped_${DateTime.now().millisecondsSinceEpoch}.jpg'; + await File(croppedPath).writeAsBytes(croppedBytes); + + // Run on-device text recognition on the cropped region only, so we + // pick up whatever brand/model/label text is inside the user's box. + final extractedText = await _recognizeText(croppedPath); + + // Send the cropped image + extracted text to the backend, which + // converts each into a vector embedding and returns both. + VectorEmbeddingResult? embeddings; + try { + embeddings = await VectorService.getEmbeddings( + imagePath: croppedPath, + labelText: extractedText, + ); + debugPrint( + 'Image vector (${embeddings.imageVector.length} dims): ${embeddings.imageVector}'); + debugPrint( + 'Text vector (${embeddings.textVector.length} dims): ${embeddings.textVector}'); + } catch (e) { + debugPrint('Vector embedding failed: $e'); + // decide whether to continue to results without vectors, or bail out + } + + // Give the "Finding similar products" sheet a moment to be visible + // before navigating — this is where a real search API call would + // happen instead of a fixed delay. + await Future.delayed(const Duration(milliseconds: 1400)); + if (!mounted) return; + + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => SearchResultsScreen( + imagePath: croppedPath, + wasDetected: true, + extractedText: extractedText, + ), + ), + ); + } catch (e) { + if (mounted) { + setState(() => _isProcessing = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not crop photo: $e')), + ); + } + } + } + + /// Runs ML Kit text recognition on [path] and returns the recognized + /// text (empty string if nothing was found or recognition failed). + Future _recognizeText(String path) async { + try { + final inputImage = InputImage.fromFilePath(path); + final result = await _textRecognizer.processImage(inputImage); + return result.text.trim(); + } catch (_) { + return ''; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF15141B), + body: SafeArea( + child: Column( + children: [ + _buildTopBar(), + const SizedBox(height: 8), + Expanded(child: _buildImageArea()), + const SizedBox(height: 16), + if (!_isProcessing) _buildHintPill(), + const SizedBox(height: 16), + ], + ), + ), + floatingActionButton: _isProcessing + ? null + : FloatingActionButton( + backgroundColor: Colors.white, + onPressed: _confirmCrop, + child: const Icon(Icons.check, color: Colors.black87), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + bottomSheet: _isProcessing ? _buildProcessingSheet() : null, + ); + } + + Widget _buildTopBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).maybePop(), + ), + const Spacer(), + Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + 'Nearle', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(width: 6), + Text( + 'ai', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontStyle: FontStyle.italic, + ), + ), + SizedBox(width: 2), + Icon(Icons.auto_awesome, color: Colors.white, size: 14), + ], + ), + const Spacer(), + const SizedBox(width: 48), // balances the back button + ], + ), + ); + } + + Widget _buildHintPill() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + _isDetecting ? 'Detecting product…' : 'Drag corners to adjust', + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ); + } + + Widget _buildImageArea() { + if (_imgWidth == null || _imgHeight == null || _cropRect == null) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + return LayoutBuilder( + builder: (context, constraints) { + final availW = constraints.maxWidth; + final availH = constraints.maxHeight; + _scale = math.min(availW / _imgWidth!, availH / _imgHeight!); + final dispW = _imgWidth! * _scale; + final dispH = _imgHeight! * _scale; + _offsetX = (availW - dispW) / 2; + _offsetY = (availH - dispH) / 2; + + final r = _cropRect!; + final screenRect = Rect.fromLTRB( + _offsetX + r.left * _scale, + _offsetY + r.top * _scale, + _offsetX + r.right * _scale, + _offsetY + r.bottom * _scale, + ); + + return Stack( + children: [ + Positioned( + left: _offsetX, + top: _offsetY, + width: dispW, + height: dispH, + child: Image.file(File(widget.imagePath), fit: BoxFit.fill), + ), + ..._buildDarkMask(screenRect, availW, availH), + Positioned.fromRect( + rect: screenRect, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) => _moveRect(d.delta), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 1.5), + ), + ), + ), + ), + ..._buildCornerHandles(screenRect), + ], + ); + }, + ); + } + + List _buildDarkMask(Rect r, double availW, double availH) { + const maskColor = Colors.black54; + return [ + Positioned( + left: 0, + top: 0, + right: 0, + height: r.top.clamp(0, availH), + child: IgnorePointer(child: Container(color: maskColor)), + ), + Positioned( + left: 0, + top: r.bottom, + right: 0, + bottom: 0, + child: IgnorePointer(child: Container(color: maskColor)), + ), + Positioned( + left: 0, + top: r.top, + width: r.left.clamp(0, availW), + height: r.height, + child: IgnorePointer(child: Container(color: maskColor)), + ), + Positioned( + left: r.right, + top: r.top, + right: 0, + height: r.height, + child: IgnorePointer(child: Container(color: maskColor)), + ), + ]; + } + + List _buildCornerHandles(Rect r) { + return [ + _cornerHandle(r.topLeft, _Corner.topLeft), + _cornerHandle(r.topRight, _Corner.topRight), + _cornerHandle(r.bottomLeft, _Corner.bottomLeft), + _cornerHandle(r.bottomRight, _Corner.bottomRight), + ]; + } + + Widget _cornerHandle(Offset position, _Corner corner) { + const hitSize = 44.0; + return Positioned( + left: position.dx - hitSize / 2, + top: position.dy - hitSize / 2, + width: hitSize, + height: hitSize, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanUpdate: (d) => _updateCorner(corner, d.delta), + child: Center( + child: CustomPaint( + size: const Size(28, 28), + painter: _CornerBracketPainter(corner: corner), + ), + ), + ), + ); + } + + Widget _buildProcessingSheet() { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(28), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 55, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(20), + ), + ), + + const SizedBox(height: 28), + + Container( + height: 70, + width: 70, + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: Center( + child: SizedBox( + width: 70, + height: 70, + child: Lottie.asset( + 'assets/lotties/ai_loader.json', + height: 140, + ), + ), + ), + ), + + const SizedBox(height: 24), + + const Text( + "Searching Similar Products", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + + const SizedBox(height: 10), + + Text( + "Analyzing image, detecting text\nand finding the closest matches...", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + height: 1.5, + ), + ), + + const SizedBox(height: 28), + + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: const LinearProgressIndicator( + minHeight: 8, + backgroundColor: Color(0xffECECEC), + color: Colors.green, + ), + ), + + const SizedBox(height: 14), + + Text( + "This usually takes a few seconds", + style: TextStyle( + color: Colors.grey.shade500, + fontSize: 12, + ), + ), + ], + ), + ); + } +} + +/// Draws a white L-shaped bracket oriented for the given corner, matching +/// the Google Lens style crop handles. +class _CornerBracketPainter extends CustomPainter { + final _Corner corner; + + _CornerBracketPainter({required this.corner}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white + ..strokeWidth = 3 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + final path = Path(); + switch (corner) { + case _Corner.topLeft: + path.moveTo(0, size.height); + path.lineTo(0, 0); + path.lineTo(size.width, 0); + break; + case _Corner.topRight: + path.moveTo(0, 0); + path.lineTo(size.width, 0); + path.lineTo(size.width, size.height); + break; + case _Corner.bottomLeft: + path.moveTo(0, 0); + path.lineTo(0, size.height); + path.lineTo(size.width, size.height); + break; + case _Corner.bottomRight: + path.moveTo(size.width, 0); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + break; + } + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _CornerBracketPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/features/dashboard/presentation/screens/voice_assistant_screen.dart b/lib/features/dashboard/presentation/screens/voice_assistant_screen.dart new file mode 100644 index 0000000..8254392 --- /dev/null +++ b/lib/features/dashboard/presentation/screens/voice_assistant_screen.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:speech_to_text/speech_to_text.dart' as stt; + +// Adjust these import paths to match your project structure. +import '../../../../constants/color_constants.dart'; +import '../../../../widgets/text_widget.dart'; +import 'ai_processing_screen.dart'; + +// TODO: replace with your actual search/results screen import + widget. +// import '../search/search_result_screen.dart'; + +class VoiceAssistantScreen extends StatefulWidget { + const VoiceAssistantScreen({super.key}); + + @override + State createState() => _VoiceAssistantScreenState(); +} + +class _VoiceAssistantScreenState extends State + with SingleTickerProviderStateMixin { + final stt.SpeechToText _speech = stt.SpeechToText(); + + bool _speechAvailable = false; + bool isListening = true; + String recognizedText = "Tap the mic and start speaking"; + + late final AnimationController controller; + + @override + void initState() { + super.initState(); + + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + _initSpeech(); + } + + @override + void dispose() { + controller.dispose(); + _speech.stop(); + super.dispose(); + } + + // ---------------- SPEECH SETUP ---------------- + + Future _initSpeech() async { + _speechAvailable = await _speech.initialize( + onStatus: (status) { + // Fires when recognition stops on its own (silence / done) + if (status == 'notListening' || status == 'done') { + if (isListening) { + setState(() => isListening = false); + _handleFinalResult(); + } + } + }, + onError: (error) { + setState(() => isListening = false); + }, + ); + setState(() {}); + } + + Future _toggleListening() async { + if (!_speechAvailable) return; + + if (isListening) { + await _speech.stop(); + setState(() => isListening = false); + _handleFinalResult(); + return; + } + + setState(() { + isListening = true; + recognizedText = "Listening..."; + }); + + await _speech.listen( + onResult: (result) { + setState(() { + recognizedText = result.recognizedWords; + }); + }, + ); + } + + /// Called once listening stops (either the user tapped stop, or the + /// recognizer auto-stopped after silence). Navigates forward with + /// whatever was captured, as long as it isn't empty. + void _handleFinalResult() { + final query = recognizedText.trim(); + + final isPlaceholder = query.isEmpty || + query == "Listening..." || + query == "Tap the mic and start speaking"; + + if (isPlaceholder) return; + + // TODO: swap this for your real navigation, e.g.: + // Navigator.pushReplacement( + // context, + // MaterialPageRoute( + // builder: (_) => SearchResultScreen(query: query), + // ), + // ); + // + // If this screen was opened for a result (e.g. from a search bar), + // you can instead just pop with the value: + // Navigator.pop(context, query); + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => AIAssistantScreen(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: ColorConstants.secondaryColor, + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildHeader(context), + const SizedBox(height: 4), + _buildStatusChip(), + const SizedBox(height: 26), + _buildMicStage(), + const SizedBox(height: 26), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: ReusableTextWidget( + text: recognizedText, + fontSize: 16, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + color: ColorConstants.blackColor, + ), + ), + + _buildPremiumMicButton(), + const SizedBox(height: 22), + ], + ), + ), + ); + } + + // ---------------- HEADER ---------------- + + Widget _buildHeader(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(18, 14, 18, 0), + child: Row( + children: [ + InkWell( + onTap: () => Navigator.maybePop(context), + borderRadius: BorderRadius.circular(30), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: ColorConstants.lightGreyBg, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 16, + color: ColorConstants.darkGreyColor, + ), + ), + ), + const Expanded( + child: Center( + child: ReusableTextWidget( + text: "Voice Assistant", + fontSize: 18, + fontWeight: FontWeight.w700, + color: ColorConstants.blackColor, + ), + ), + ), + const SizedBox(width: 40), + ], + ), + ); + } + + Widget _buildStatusChip() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: ColorConstants.primaryColor1, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 8, + width: 8, + decoration: const BoxDecoration( + color: ColorConstants.primaryColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + ReusableTextWidget( + text: isListening ? "Listening" : "Paused", + fontSize: 12, + fontWeight: FontWeight.w600, + color: ColorConstants.primaryColor, + ), + ], + ), + ); + } + + // ---------------- MIC / LOTTIE ---------------- + + Widget _buildMicStage() { + return SizedBox( + height: 270, + width: 270, + child: Center( + child: Lottie.asset( + "assets/lotties/ai.json", + height: 250, + width: 250, + repeat: true, + animate: isListening, + ), + ), + ); + } + + // ---------------- PREMIUM MIC BUTTON ---------------- + + Widget _buildPremiumMicButton() { + return Column( + children: [ + GestureDetector( + onTap: _toggleListening, + child: AnimatedBuilder( + animation: controller, + builder: (context, child) { + final ringOpacity = + isListening ? 0.18 + (controller.value * 0.18) : 0.0; + return Container( + height: 92, + width: 92, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + ColorConstants.primaryColor1.withOpacity(ringOpacity), + ), + child: Center(child: child), + ); + }, + child: Container( + height: 68, + width: 68, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ColorConstants.primaryColor, Color(0xFF8C3EAE)], + ), + boxShadow: [ + BoxShadow( + color: ColorConstants.primaryColor.withOpacity(0.4), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Icon( + isListening ? Icons.mic_rounded : Icons.mic_off_rounded, + color: ColorConstants.secondaryColor, + size: 28, + ), + ), + ), + ), + const SizedBox(height: 12), + ReusableTextWidget( + text: !_speechAvailable + ? "Speech recognition unavailable" + : isListening + ? "Tap to stop listening" + : "Tap to speak", + fontSize: 13, + fontWeight: FontWeight.w500, + color: ColorConstants.lightGrey, + ), + ], + ); + } +} + +/// Temporary placeholder so the file compiles and navigation is visible. +/// Delete this and point `_handleFinalResult` at your real screen. +class _PlaceholderResultScreen extends StatelessWidget { + final String query; + + const _PlaceholderResultScreen({required this.query}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Search Results")), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'You said: "$query"\n\nReplace _PlaceholderResultScreen with your real search/results page.', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/orders/presentation/order_tracking.dart b/lib/features/orders/presentation/order_tracking.dart new file mode 100644 index 0000000..cfd7fdc --- /dev/null +++ b/lib/features/orders/presentation/order_tracking.dart @@ -0,0 +1,744 @@ +// delivery_tracking_page.dart +// +// SETUP: +// 1. pubspec.yaml -> add: +// google_maps_flutter: ^2.9.0 +// url_launcher: ^6.3.0 (for calling the delivery partner) +// +// 2. Android -> android/app/src/main/AndroidManifest.xml, inside : +// +// +// 3. iOS -> ios/Runner/AppDelegate.swift, inside application(didFinishLaunchingWithOptions): +// GMSServices.provideAPIKey("xxx") +// +// Replace "xxx" above with your real Google Maps API key in BOTH places. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../widgets/text_widget.dart'; + +/// Draws a small circular badge marker at runtime (no external icon assets +/// needed) — a colored circle with an icon inside, similar to Swiggy's +/// bike / restaurant / home pins. +Future _drawBadgeMarker({ + required IconData icon, + required Color background, + Color iconColor = Colors.white, + double size = 60, +}) async { + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + final paint = Paint()..color = background; + final radius = size / 2; + + canvas.drawCircle(Offset(radius, radius), radius, Paint()..color = Colors.white); + canvas.drawCircle(Offset(radius, radius), radius - 6, paint); + + final textPainter = TextPainter(textDirection: TextDirection.ltr); + textPainter.text = TextSpan( + text: String.fromCharCode(icon.codePoint), + style: TextStyle( + fontSize: size * 0.5, + fontFamily: icon.fontFamily, + package: icon.fontPackage, + color: iconColor, + ), + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset(radius - textPainter.width / 2, radius - textPainter.height / 2), + ); + + final picture = recorder.endRecording(); + final img = await picture.toImage(size.toInt(), size.toInt()); + final bytes = await img.toByteData(format: ui.ImageByteFormat.png); + return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List()); +} + +/// Your Google Maps API key (also enable it for "Directions API"). +const String kGoogleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q"; + +/// Custom map style JSON — a clean, low-saturation grey theme. +const String kSwiggyLikeMapStyle = ''' +[ + {"elementType": "geometry", "stylers": [{"color": "#f5f5f3"}]}, + {"elementType": "labels.icon", "stylers": [{"visibility": "off"}]}, + {"elementType": "labels.text.fill", "stylers": [{"color": "#8a8a8a"}]}, + {"elementType": "labels.text.stroke", "stylers": [{"color": "#f5f5f3"}]}, + {"featureType": "administrative", "elementType": "geometry", "stylers": [{"visibility": "off"}]}, + {"featureType": "administrative.land_parcel", "stylers": [{"visibility": "off"}]}, + {"featureType": "administrative.neighborhood", "stylers": [{"visibility": "off"}]}, + {"featureType": "poi", "stylers": [{"visibility": "off"}]}, + {"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#e5e8e0"}]}, + {"featureType": "road", "elementType": "geometry", "stylers": [{"color": "#ffffff"}]}, + {"featureType": "road", "elementType": "geometry.stroke", "stylers": [{"color": "#e3e3e3"}]}, + {"featureType": "road.arterial", "elementType": "labels", "stylers": [{"visibility": "off"}]}, + {"featureType": "road.highway", "elementType": "geometry", "stylers": [{"color": "#f2c9a0"}]}, + {"featureType": "road.local", "elementType": "labels", "stylers": [{"visibility": "off"}]}, + {"featureType": "transit", "stylers": [{"visibility": "off"}]}, + {"featureType": "water", "elementType": "geometry", "stylers": [{"color": "#c9d6df"}]} +] +'''; + +class DeliveryTrackingPage extends StatefulWidget { + const DeliveryTrackingPage({super.key}); + + @override + State createState() => _DeliveryTrackingPageState(); +} + +class _DeliveryTrackingPageState extends State { + GoogleMapController? _mapController; + + String _partnerName = 'Majid Khan'; + double _partnerRating = 4.8; + int _partnerOrders = 1240; + + // Sample coordinates - replace with your real pickup / drop / rider coords + final LatLng _pickup = const LatLng(28.6450, 77.3550); // restaurant + final LatLng _drop = const LatLng(28.6395, 77.3480); // customer + LatLng _riderPosition = const LatLng(28.6440, 77.3520); + + final Set _markers = {}; + final Set _polylines = {}; + List _routePoints = []; + + Timer? _riderTimer; + int _routeIndex = 0; + int _etaMinutes = 2; + + BitmapDescriptor? _pickupIcon; + BitmapDescriptor? _dropIcon; + BitmapDescriptor? _riderIcon; + + @override + void initState() { + super.initState(); + _loadIcons(); + } + + Future _loadIcons() async { + _pickupIcon = await _drawBadgeMarker( + icon: Icons.storefront, + background: const Color(0xFFFF6A00), + ); + _dropIcon = await _drawBadgeMarker( + icon: Icons.home, + background: const Color(0xFF7A1F5C), + ); + _riderIcon = await _drawBadgeMarker( + icon: Icons.two_wheeler, + background: const Color(0xFF662582), + size: 60, + ); + _setupMarkers(); + _fetchRoute(); + } + + @override + void dispose() { + _riderTimer?.cancel(); + super.dispose(); + } + + void _setupMarkers() { + _markers + ..clear() + ..addAll([ + Marker( + markerId: const MarkerId('pickup'), + position: _pickup, + icon: _pickupIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueOrange), + infoWindow: const InfoWindow(title: 'Wrapperz'), + ), + Marker( + markerId: const MarkerId('drop'), + position: _drop, + icon: _dropIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueViolet), + infoWindow: const InfoWindow(title: 'Friends and Family'), + ), + Marker( + markerId: const MarkerId('rider'), + position: _riderPosition, + icon: _riderIcon ?? + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + infoWindow: const InfoWindow(title: 'MAJID - delivery partner'), + ), + ]); + } + + /// Fetches a real road-following route from Google Directions API and + /// draws it as a polyline. + Future _fetchRoute() async { + try { + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json' + '?origin=${_pickup.latitude},${_pickup.longitude}' + '&destination=${_drop.latitude},${_drop.longitude}' + '&mode=driving' + '&key=$kGoogleApiKey', + ); + + final response = await http.get(url); + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['status'] == 'OK') { + final points = data['routes'][0]['overview_polyline']['points']; + _routePoints = _decodePolyline(points); + } else { + _routePoints = _fallbackRoute(); + } + } else { + _routePoints = _fallbackRoute(); + } + } catch (_) { + _routePoints = _fallbackRoute(); + } + + setState(() { + _polylines + ..clear() + ..add( + Polyline( + polylineId: const PolylineId('route'), + points: _routePoints, + color: const Color(0xFF662582), + width: 5, + startCap: Cap.roundCap, + endCap: Cap.roundCap, + jointType: JointType.round, + ), + ); + }); + + _startRiderAnimation(); + } + + /// Simple zig-zag fallback so the UI still shows a route without a live key. + List _fallbackRoute() { + return [ + _pickup, + LatLng(_pickup.latitude - 0.0025, _pickup.longitude - 0.0015), + LatLng(_pickup.latitude - 0.0035, _pickup.longitude - 0.0060), + LatLng(_pickup.latitude - 0.0060, _pickup.longitude - 0.0075), + _drop, + ]; + } + + List _decodePolyline(String encoded) { + List points = []; + int index = 0, len = encoded.length; + int lat = 0, lng = 0; + + while (index < len) { + int b, shift = 0, result = 0; + do { + b = encoded.codeUnitAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + int dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + lat += dlat; + + shift = 0; + result = 0; + do { + b = encoded.codeUnitAt(index++) - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + int dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + lng += dlng; + + points.add(LatLng(lat / 1E5, lng / 1E5)); + } + return points; + } + + /// Moves the rider marker along the route to simulate live tracking. + void _startRiderAnimation() { + if (_routePoints.isEmpty) return; + _riderTimer?.cancel(); + _riderTimer = Timer.periodic(const Duration(seconds: 2), (timer) { + if (_routeIndex >= _routePoints.length - 1) { + timer.cancel(); + return; + } + _routeIndex++; + _riderPosition = _routePoints[_routeIndex]; + _setupMarkers(); + setState(() {}); + _mapController?.animateCamera(CameraUpdate.newLatLng(_riderPosition)); + }); + } + + Future _callPartner() async { + final uri = Uri(scheme: 'tel', path: '+910000000000'); + if (await canLaunchUrl(uri)) await launchUrl(uri); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: SizedBox(), + + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ReusableTextWidget( + text: 'Out for delivery', + fontSize: 17, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + const SizedBox(height: 2), + ReusableTextWidget( + text: '11:38 AM • 1 items', + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.white + ), + ], + ), + centerTitle: true, + backgroundColor: const Color(0xFF662582), + ), + backgroundColor: Colors.white, + body: Stack( + children: [ + // ---------------- MAP ---------------- + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: GoogleMap( + initialCameraPosition: CameraPosition( + target: _pickup, + zoom: 14.5, + ), + markers: _markers, + polylines: _polylines, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + compassEnabled: false, + mapToolbarEnabled: false, + buildingsEnabled: false, + indoorViewEnabled: false, + trafficEnabled: false, + style: kSwiggyLikeMapStyle, + onMapCreated: (controller) => _mapController = controller, + ), + ), + + // -------------- TOP BAR -------------- + + + // SafeArea( + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _circleIconButton(Icons.arrow_back, () { + // Navigator.maybePop(context); + // }), + // Column( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // ReusableTextWidget( + // text: 'Out for delivery', + // fontSize: 17, + // fontWeight: FontWeight.w700, + // color: Colors.grey.shade900, + // ), + // const SizedBox(height: 2), + // ReusableTextWidget( + // text: '11:38 AM • 1 items', + // fontSize: 12, + // fontWeight: FontWeight.w500, + // color: Colors.grey.shade600, + // ), + // ], + // ), + // const SizedBox(width: 40), + // ], + // ), + // ), + // ), + + // -------------- BOTTOM SHEET -------------- + DraggableScrollableSheet( + initialChildSize: 0.42, + minChildSize: 0.42, + maxChildSize: 0.9, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + // Out for delivery row + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: 'Out for delivery', + fontSize: 20, + fontWeight: FontWeight.w700, + color: Colors.grey.shade800, + ), + const SizedBox(height: 4), + ReusableTextWidget( + text: 'MAJID is on the way to deliver your order', + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFF7A1F5C), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + ReusableTextWidget( + text: '$_etaMinutes', + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ReusableTextWidget( + text: 'mins', + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ], + ), + ), + ], + ), + ), + // Delivery partner info card + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 28, + backgroundColor: Colors.grey.shade100, + backgroundImage: const NetworkImage( + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSmbomkQ5Aqtd848F_UVR86eY1wrq5tuNJRjmRXAGgXsqJtlL5H6EF8Efws&s=10'), + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.green.shade500, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: _partnerName, + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.grey.shade800, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.star_rounded, + size: 16, color: Colors.amber.shade700), + const SizedBox(width: 4), + ReusableTextWidget( + text: '$_partnerRating', + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + const SizedBox(width: 10), + ReusableTextWidget( + text: '• $_partnerOrders orders', + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ], + ), + ], + ), + ), + InkWell( + onTap: _callPartner, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFFCE4EC), + ), + child: const Icon(Icons.call, + color: Colors.redAccent, size: 18), + ), + ), + ], + ), + const SizedBox(height: 14), + + // Action buttons row + ], + ), + ), + ), + + + + + Container(height: 8, color: const Color(0xFFF5F5F5)), + + // Product / order items card + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 20), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon(Icons.receipt_long, size: 20, color: Colors.grey.shade700), + const SizedBox(width: 8), + ReusableTextWidget( + text: 'Order ID: # 123456789', + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ], + ), + const SizedBox(height: 12), + const Divider(height: 1), + const SizedBox(height: 12), + + // Items list + _buildBillItem('Chicken Biryani', 1, 250), + const SizedBox(height: 10), + _buildBillItem('Mutton Biryani', 1, 350), + const SizedBox(height: 10), + + const SizedBox(height: 4), + const Divider(height: 1), + const SizedBox(height: 12), + + // Price breakdown + _buildPriceRow('Subtotal', 600), + const SizedBox(height: 6), + _buildPriceRow('Delivery Fee', 40), + const SizedBox(height: 6), + _buildPriceRow('Tax (5%)', 30), + const SizedBox(height: 12), + const Divider(height: 1), + const SizedBox(height: 12), + + // Total + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ReusableTextWidget( + text: 'Total', + fontSize: 15, + fontWeight: FontWeight.w700, + color: Colors.grey.shade900, + ), + ReusableTextWidget( + text: '₹670', + fontSize: 15, + fontWeight: FontWeight.w700, + color: Colors.grey.shade900, + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + ReusableTextWidget( + text: label, + fontSize: 13, + fontWeight: FontWeight.w600, + color: color, + ), + ], + ), + ), + ); + } + + Widget _buildBillItem(String name, int qty, double price) { + final total = qty * price; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ReusableTextWidget( + text: '${qty}x $name', + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ReusableTextWidget( + text: '₹${price.toStringAsFixed(0)}', + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + const SizedBox(width: 12), + ReusableTextWidget( + text: '₹${total.toStringAsFixed(0)}', + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ], + ); + } + + Widget _buildPriceRow(String label, double amount) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ReusableTextWidget( + text: label, + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ReusableTextWidget( + text: '₹${amount.toStringAsFixed(0)}', + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ], + ); + } + + Widget _circleIconButton(IconData icon, VoidCallback onTap) { + return InkWell( + onTap: onTap, + customBorder: const CircleBorder(), + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 6), + ], + ), + child: Icon(icon, color: Colors.black87, size: 20), + ), + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 87e2cd8..4303f0b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; -import 'dart:io'; - import 'package:alp_animated_splashscreen/alp_animated_splashscreen.dart'; +import 'package:app_links/app_links.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; @@ -10,11 +9,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; +import 'package:in_app_update/in_app_update.dart'; import 'package:nearledaily/constants/color_constants.dart'; import 'package:nearledaily/view/authentication/app_update_view.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; + import 'package:package_info_plus/package_info_plus.dart'; import 'package:new_version_plus/new_version_plus.dart'; @@ -26,35 +27,27 @@ import 'controllers/dashboard_controller/dashboard_controller.dart'; import 'controllers/intro_controller/intro_screen_controller.dart'; import 'controllers/order_controller/create_order_controller.dart'; import 'controllers/tenant/get_tenant.dart'; -import 'controllers/tenant_controller /tenant_list.dart'; -import 'helper/firebase_options.dart'; +import 'controllers/tenant_list/tenant_controller.dart'; + +import 'core/services/ExitWrapper.dart'; +import 'features/dashboard/presentation/screens/ai_processing_screen.dart'; import 'service/bindings.dart'; import 'service/device_info/device_info.dart'; import 'view/home_view.dart'; import 'view/splash_view/splash_view.dart'; -// ------------------------- -// FIREBASE BACKGROUND HANDLER -// ------------------------- @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); - - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - + await Firebase.initializeApp(); const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); - await flutterLocalNotificationsPlugin.initialize( const InitializationSettings(android: androidSettings), ); final androidPlugin = flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation(); - + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); await androidPlugin?.createNotificationChannel( const AndroidNotificationChannel( 'nearle_channel', @@ -67,81 +60,56 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { final title = data['title'] as String?; final body = data['body'] as String?; - if (title != null && title.isNotEmpty) { - await flutterLocalNotificationsPlugin.show( - 999, - title, - body?.isNotEmpty == true ? body : null, - NotificationDetails( - android: AndroidNotificationDetails( - 'nearle_channel', - 'Nearle Notifications', - importance: Importance.max, - priority: Priority.high, - fullScreenIntent: true, - playSound: true, - enableVibration: true, - largeIcon: const DrawableResourceAndroidBitmap('nearle_logo.jpeg'), - ), + if (title == null || title.isEmpty) return; + + await flutterLocalNotificationsPlugin.show( + 999, + title, + body?.isNotEmpty == true ? body : null, + NotificationDetails( + android: AndroidNotificationDetails( + 'nearle_channel', + 'Nearle Notifications', + importance: Importance.max, + priority: Priority.high, + fullScreenIntent: true, + playSound: true, + enableVibration: true, + largeIcon: const DrawableResourceAndroidBitmap('nearle_logo.jpeg'), ), - ); - } + ), + ); } -// ------------------------- -// LOCAL NOTIFICATIONS SETUP -// ------------------------- final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future _setupLocalNotifications() async { - // Android settings const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); - - // iOS/macOS settings - const DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings( - requestSoundPermission: true, - requestBadgePermission: true, - requestAlertPermission: true, - ); - - // Combine all platforms - const InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - macOS: initializationSettingsIOS, - ); - + const InitializationSettings initializationSettings = + InitializationSettings(android: initializationSettingsAndroid); await flutterLocalNotificationsPlugin.initialize(initializationSettings); - // Android-only notification channel - if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { - const AndroidNotificationChannel channel = AndroidNotificationChannel( - 'nearle_channel', - 'Nearle Notifications', - description: 'High priority notifications', - importance: Importance.max, - playSound: true, - enableVibration: true, - ); + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'nearle_channel', + 'Nearle Notifications', + description: 'High priority notifications', + importance: Importance.max, + playSound: true, + enableVibration: true, + ); - await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() - ?.createNotificationChannel(channel); - } + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); } -// ------------------------- -// ROUTE OBSERVER -// ------------------------- final RouteObserver> routeObserver = RouteObserver>(); -// ------------------------- -// APP VERSION CHECK -// ------------------------- +// PRINT CURRENT + STORE VERSION (unchanged) Future _printAndCheckAppVersions() async { try { final PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -159,7 +127,10 @@ Future _printAndCheckAppVersions() async { await prefs.setString('currentAppVersion', currentVersion); if (defaultTargetPlatform == TargetPlatform.android) { - final newVersion = NewVersionPlus(androidId: "com.nearle.gear"); + final newVersion = NewVersionPlus( + androidId: "com.nearle.gear", + ); + final status = await newVersion.getVersionStatus(); if (status != null) { print("PLAY STORE VERSION: ${status.storeVersion}"); @@ -179,7 +150,7 @@ Future _printAndCheckAppVersions() async { } } -// Returns true if update available +// FIXED: Now correctly detects real update from Play Store Future _checkForAppUpdate() async { if (defaultTargetPlatform != TargetPlatform.android) return false; @@ -190,24 +161,18 @@ Future _checkForAppUpdate() async { if (status != null && status.canUpdate) { print("UPDATE DETECTED! Redirecting to AppUpdateView"); print("Current: ${status.localVersion} → Store: ${status.storeVersion}"); - return true; + return true; // This will now go to AppUpdateView } } catch (e) { - print("Version check failed: $e"); + print("Version check failed (continuing anyway): $e"); } return false; } -// ------------------------- -// HANDLE NOTIFICATION TAP -// ------------------------- void _handleMessage(RemoteMessage message) { print("Notification tapped: ${message.data}"); } -// ------------------------- -// ANIMATED SPLASH -// ------------------------- class AnimatedSplashWithNavigation extends StatefulWidget { final Widget nextScreen; const AnimatedSplashWithNavigation({super.key, required this.nextScreen}); @@ -237,162 +202,166 @@ class _AnimatedSplashWithNavigationState backgroundcolor: Colors.white, foregroundcolor: ColorConstants.primaryColor, brandnamecolor: ColorConstants.primaryColor, + // companyname: 'Nearle Daily', ); } } -// ------------------------- -// MAIN ENTRY POINT -// ------------------------- + + Future main() async { WidgetsFlutterBinding.ensureInitialized(); - // Firebase initialize with options (iOS/web safe) - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + FlutterError.onError = (details) => FlutterError.dumpErrorToConsole(details); - await _printAndCheckAppVersions(); - await _setupLocalNotifications(); + await runZonedGuarded(() async { - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle(statusBarColor: Colors.grey[200]), - ); - final prefs = await SharedPreferences.getInstance(); - prefs.setBool("firstTime", true); + await Firebase.initializeApp(); - // ------------------------- - // NOTIFICATION PERMISSIONS & FCM TOKEN - // ------------------------- - if (!kIsWeb) { - if (Platform.isIOS) { - NotificationSettings settings = + + + + + await _printAndCheckAppVersions(); + + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle(statusBarColor: Colors.grey[200]), + ); + + await _setupLocalNotifications(); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + + RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); + if (initialMessage != null) _handleMessage(initialMessage); + + FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); + + if (!kIsWeb) { await FirebaseMessaging.instance.requestPermission( alert: true, badge: true, sound: true, ); - - if (settings.authorizationStatus == AuthorizationStatus.authorized) { - print('User granted permission'); - - // Listen for token refresh (APNs ready) - FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async { - print('FCM Token (iOS APNs ready): $newToken'); - await prefs.setString('fcmToken', newToken); - }); - - // Optional: small delay initial attempt - Future.delayed(const Duration(seconds: 2), () async { - String? token = await FirebaseMessaging.instance.getToken(); - print('FCM Token initial attempt (iOS): $token'); - if (token != null) await prefs.setString('fcmToken', token); - }); - } else { - print('User declined notification permission'); - } - } else { - // Android: get token immediately - String? token = await FirebaseMessaging.instance.getToken(); - print('FCM Token (Android): $token'); - if (token != null) await prefs.setString('fcmToken', token); } - } + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool("firstTime", true); - // Foreground notifications - FirebaseMessaging.onMessage.listen((RemoteMessage message) async { - final data = message.data; - final title = data['title'] ?? message.notification?.title ?? 'Nearle'; - final body = data['body'] ?? message.notification?.body ?? ''; + FirebaseMessaging.onMessage.listen((RemoteMessage message) async { + final data = message.data; + final title = data['title'] ?? message.notification?.title ?? 'Nearle'; + final body = data['body'] ?? message.notification?.body ?? ''; - final ByteData jpegData = - await rootBundle.load('assets/images/nearledaily.png'); - final Uint8List jpegBytes = jpegData.buffer.asUint8List(); + final ByteData jpegData = + await rootBundle.load('assets/images/nearledaily.png'); + final Uint8List jpegBytes = jpegData.buffer.asUint8List(); - await flutterLocalNotificationsPlugin.show( - DateTime.now().millisecondsSinceEpoch ~/ 1000, - title, - body, - NotificationDetails( - android: AndroidNotificationDetails( - 'nearle_channel', - 'Nearle Notifications', - importance: Importance.max, - priority: Priority.high, - fullScreenIntent: true, - playSound: true, - enableVibration: true, - largeIcon: ByteArrayAndroidBitmap(jpegBytes), + await flutterLocalNotificationsPlugin.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + title, + body, + NotificationDetails( + android: AndroidNotificationDetails( + 'nearle_channel', + 'Nearle Notifications', + importance: Importance.max, + priority: Priority.high, + fullScreenIntent: true, + playSound: true, + enableVibration: true, + largeIcon: ByteArrayAndroidBitmap(jpegBytes), + ), ), - iOS: const DarwinNotificationDetails(), + ); + }); + + String? fcmToken = await FirebaseMessaging.instance.getToken(); + await prefs.setString('fcmToken', fcmToken ?? ''); + + if (defaultTargetPlatform == TargetPlatform.android) { + final params = PlatformWebViewControllerCreationParams(); + AndroidWebViewController(params); + } + + ApiConstants.tenantCustomers = ApiConstants.tenantCustomerLive; + ApiConstants.orderedtenantCustomers = ApiConstants.orderedtenantCustomerLive; + ApiConstants.login = ApiConstants.loginLive; + + Get.put(TenantController(), permanent: true); + Get.lazyPut(() => AuthController(), fenix: true); + Get.lazyPut(() => DashboardController(), fenix: true); + Get.lazyPut(() => CartController(), fenix: true); + Get.lazyPut(() => BottomNavController(), fenix: true); + Get.lazyPut(() => OrderedTenantController(), fenix: true); + Get.lazyPut(() => OrderController(), fenix: true); + Get.lazyPut(() => FaqController(), fenix: true); + Get.lazyPut(() => IntroScreenController(), fenix: true); + + + + DeviceInfo deviceInfo = DeviceInfo(); + await deviceInfo.getDeviceInfo(); + + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + final int? customerId = prefs.getInt('customerId'); + final String? contactNo = prefs.getString('contactno'); + bool updateAvailable = await _checkForAppUpdate(); // Now works correctly! + + Widget nextScreen; + if (updateAvailable) { + nextScreen = const AppUpdateView(); + } else if (customerId != null && contactNo != null && contactNo.isNotEmpty) { + nextScreen = BottomNavigation(); + } else { + nextScreen = const SplashScreenView(); + } + // --- Deep link check, overrides nextScreen if a valid link exists --- + try { + final initialUri = await AppLinks().getInitialLink(); + debugPrint('DEEPLINK initial: $initialUri'); + if (initialUri != null && + initialUri.scheme == 'nearle' && + initialUri.host == 'order') { + final item = initialUri.queryParameters['item']; + final budget = initialUri.queryParameters['budget']; + nextScreen = AIAssistantScreen(); + } + } catch (e) { + debugPrint('DEEPLINK initial error: $e'); + } + + + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.white, // or transparent + statusBarIconBrightness: Brightness.dark, // Android + statusBarBrightness: Brightness.light, // iOS ), ); - }); - // Android WebView Controller - if (defaultTargetPlatform == TargetPlatform.android) { - final params = PlatformWebViewControllerCreationParams(); - AndroidWebViewController(params); - } - - // API constants - ApiConstants.tenantCustomers = ApiConstants.tenantCustomerLive; - ApiConstants.orderedtenantCustomers = ApiConstants.orderedtenantCustomerLive; - ApiConstants.login = ApiConstants.loginLive; - - // Controllers - Get.put(TenantController(), permanent: true); - Get.lazyPut(() => AuthController(), fenix: true); - Get.lazyPut(() => DashboardController(), fenix: true); - Get.lazyPut(() => CartController(), fenix: true); - Get.lazyPut(() => BottomNavController(), fenix: true); - Get.lazyPut(() => OrderedTenantController(), fenix: true); - Get.lazyPut(() => OrderController(), fenix: true); - Get.lazyPut(() => FaqController(), fenix: true); - Get.lazyPut(() => IntroScreenController(), fenix: true); - - DeviceInfo deviceInfo = DeviceInfo(); - await deviceInfo.getDeviceInfo(); - - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - final int? customerId = prefs.getInt('customerId'); - final String? contactNo = prefs.getString('contactno'); - bool updateAvailable = await _checkForAppUpdate(); - - Widget nextScreen; - if (updateAvailable) { - nextScreen = const AppUpdateView(); - } else if (customerId != null && contactNo != null && contactNo.isNotEmpty) { - nextScreen = BottomNavigation(); - } else { - nextScreen = const SplashScreenView(); - } - - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.white, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - ), - ); - - runApp(MyApp(startScreen: nextScreen)); + runApp(MyApp(startScreen: nextScreen)); + AppLinks().uriLinkStream.listen((uri) { + debugPrint('DEEPLINK stream: $uri'); + if (uri.scheme == 'nearle' && uri.host == 'order') { + final item = uri.queryParameters['item']; + final budget = uri.queryParameters['budget']; + Get.to(() => AIAssistantScreen()); + } + }); + }, (error, stack) => print('Error: $error')); } -// ------------------------- -// MAIN APP -// ------------------------- class MyApp extends StatelessWidget { final Widget startScreen; const MyApp({super.key, required this.startScreen}); + @override Widget build(BuildContext context) { return GetMaterialApp( @@ -404,6 +373,7 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), initialBinding: GlobalBinding(), + home: AnimatedSplashWithNavigation(nextScreen: startScreen), ); } diff --git a/lib/modules/search/grocery_item.dart b/lib/modules/search/grocery_item.dart new file mode 100644 index 0000000..575c1e2 --- /dev/null +++ b/lib/modules/search/grocery_item.dart @@ -0,0 +1,24 @@ +class GroceryItem { + final int id; + final String name; + final String quantity; + final String emoji; + bool inCart; + + GroceryItem({ + required this.id, + required this.name, + required this.quantity, + required this.emoji, + this.inCart = false, + }); + + factory GroceryItem.fromJson(int id, Map json) { + return GroceryItem( + id: id, + name: json['name'] ?? '', + quantity: json['quantity'] ?? '', + emoji: json['emoji'] ?? '🛒', + ); + } +} \ No newline at end of file diff --git a/lib/modules/search_model/search_model.dart b/lib/modules/search_model/search_model.dart new file mode 100644 index 0000000..d6407dd --- /dev/null +++ b/lib/modules/search_model/search_model.dart @@ -0,0 +1,69 @@ +// ─── Data Model ─────────────────────────────────────────────────────────────── + +import 'dart:ui'; + +class GroceryItem { + final int id; + final String name; + final String quantity; + final String emoji; + bool inCart; + + GroceryItem({ + required this.id, + required this.name, + required this.quantity, + required this.emoji, + this.inCart = false, + }); + + factory GroceryItem.fromJson(int id, Map json) { + return GroceryItem( + id: id, + name: json['name']?.toString() ?? '', + quantity: json['quantity']?.toString() ?? '', + emoji: json['emoji']?.toString() ?? '🛒', + ); + } +} + +class RecommendedItem { + final String title; + final String subtitle; + final String image; + final Color bgColor; + final Color accentColor; + + RecommendedItem({ + required this.title, + required this.subtitle, + required this.image, + required this.bgColor, + required this.accentColor, + }); +} + + +class ProductItem { + final String name; + final String category; + final String price; + final String originalPrice; + final String image; + final double rating; + final int reviewCount; + final bool isFavorite; + + ProductItem({ + required this.name, + required this.category, + required this.price, + required this.originalPrice, + required this.image, + required this.rating, + required this.reviewCount, + this.isFavorite = false, + }); +} + + diff --git a/lib/service/ai_service/api_service.dart b/lib/service/ai_service/api_service.dart new file mode 100644 index 0000000..b3f52af --- /dev/null +++ b/lib/service/ai_service/api_service.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class GroqService { + static const String _apiKey = 'YOUR_API_KEY'; + + static Future fetchIngredients(String query) async { + final res = await http.post( + Uri.parse('https://api.groq.com/openai/v1/chat/completions'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $_apiKey', + }, + body: jsonEncode({ + 'model': 'llama-3.1-8b-instant', + 'max_tokens': 600, + 'messages': [ + { + 'role': 'system', + 'content': + 'Return ONLY JSON array of grocery items...' + }, + {'role': 'user', 'content': query} + ], + }), + ); + + final data = jsonDecode(res.body); + return data['choices'][0]['message']['content']; + } +} + + +class AiSearchService { + static const String baseUrl = + 'http://10.0.2.2:8000/find-ingredients'; + + Future> search(String query) async { + final response = await http.post( + Uri.parse(baseUrl), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + "user_input": query, + "customer_id": 6060, + "latitude": 11.0050728, + "longitude": 76.9508513 + }), + ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } + + throw Exception("Failed to load results"); + } +} \ No newline at end of file diff --git a/lib/service/ai_service/product_service.dart b/lib/service/ai_service/product_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/service/ai_service/store_service.dart b/lib/service/ai_service/store_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/service/firebase_analytics/analytics_service.dart b/lib/service/firebase_analytics/analytics_service.dart new file mode 100644 index 0000000..cdcc0ea --- /dev/null +++ b/lib/service/firebase_analytics/analytics_service.dart @@ -0,0 +1,28 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; + +class AnalyticsService { + static final FirebaseAnalytics analytics = + FirebaseAnalytics.instance; + + static Future logScreenView(String screenName) async { + await analytics.logScreenView( + screenName: screenName, + ); + } + + static Future logLogin(String method) async { + await analytics.logLogin( + loginMethod: method, + ); + } + + static Future logEvent( + String name, { + Map? parameters, + }) async { + await analytics.logEvent( + name: name, + parameters: parameters, + ); + } +} \ No newline at end of file diff --git a/lib/view/account/account_view.dart b/lib/view/account/account_view.dart index 67dd079..5712e21 100644 --- a/lib/view/account/account_view.dart +++ b/lib/view/account/account_view.dart @@ -277,17 +277,8 @@ class _AccountPageState extends State { } }, ), - _divider(), - _tile( - icon: Icons.question_answer, - title: "Faq", - onTap: () => Get.to( - () => FaqView(), - // transition: Transition.fade, // or any transition you like - // duration: Duration(milliseconds: 400), - ), - ), + _divider(), _tile( icon: Icons.reorder, @@ -318,15 +309,15 @@ class _AccountPageState extends State { onTap: controller.rateApp, ), _divider(), - // _tile( - // icon: Icons.group_add, - // title: "Refer a Friend", - // onTap: () => Get.to( - // () => const ShowContactsScreen(), - // // transition: Transition.fade, // or any style you like - // // duration: Duration(milliseconds: 400), - // ), - // ), + _tile( + icon: Icons.group_add, + title: "Refer a Friend", + onTap: () => Get.to( + () => ReferEarnScreen(), + // transition: Transition.fade, // or any style you like + // duration: Duration(milliseconds: 400), + ), + ), ], ), @@ -347,6 +338,17 @@ class _AccountPageState extends State { // duration: Duration(milliseconds: 400), ), + ), + _divider(), + _tile( + icon: Icons.question_answer, + title: "Faq", + onTap: () => Get.to( + () => FaqView(), + // transition: Transition.fade, // or any transition you like + // duration: Duration(milliseconds: 400), + ), + ), _divider(), _tile( diff --git a/lib/view/account/share_app.dart b/lib/view/account/share_app.dart index 093dc00..b38fb80 100644 --- a/lib/view/account/share_app.dart +++ b/lib/view/account/share_app.dart @@ -1,379 +1,396 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_contacts/flutter_contacts.dart'; -// import 'package:nearledaily/constants/color_constants.dart'; -// import 'package:permission_handler/permission_handler.dart' -// as permission_handler; -// import 'package:url_launcher/url_launcher.dart'; -// import 'package:flutter/services.dart'; -// -// import '../../constants/font_constants.dart'; -// import '../../widgets/text_widget.dart'; -// -// class ShowContactsScreen extends StatefulWidget { -// const ShowContactsScreen({super.key}); -// -// @override -// State createState() => _ShowContactsScreenState(); -// } -// -// class _ShowContactsScreenState extends State -// with WidgetsBindingObserver { -// List _contacts = []; -// bool _loading = false; -// bool _permissionDenied = false; -// -// /// 🔹 ADDED -// bool _showDisclaimer = true; -// -// @override -// void initState() { -// super.initState(); -// WidgetsBinding.instance.addObserver(this); -// _loadContacts(); -// } -// -// @override -// void dispose() { -// WidgetsBinding.instance.removeObserver(this); -// super.dispose(); -// } -// -// Future _loadContacts() async { -// setState(() { -// _loading = true; -// _permissionDenied = false; -// }); -// -// final bool granted = await FlutterContacts.requestPermission(); -// -// if (!granted) { -// setState(() { -// _loading = false; -// _permissionDenied = true; -// }); -// return; -// } -// -// try { -// final List contacts = await FlutterContacts.getContacts( -// withProperties: true, -// withPhoto: true, -// ); -// -// setState(() { -// _contacts = contacts -// .where((c) => c.phones.isNotEmpty) -// .toList() -// ..sort((a, b) => a.displayName.compareTo(b.displayName)); -// _loading = false; -// }); -// } catch (e) { -// setState(() { -// _loading = false; -// }); -// ScaffoldMessenger.of(context).showSnackBar( -// SnackBar( -// content: ReusableTextWidget( -// text: "Error loading contacts: $e", -// fontSize: 14, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.white, -// ), -// ), -// ); -// } -// } -// -// Widget _buildAvatar(Contact contact) { -// if (contact.photo != null && contact.photo!.isNotEmpty) { -// return CircleAvatar( -// backgroundImage: MemoryImage(contact.photo!), -// ); -// } else { -// String initials = ""; -// final names = contact.displayName.split(" "); -// if (names.isNotEmpty) initials += names[0][0]; -// if (names.length > 1) initials += names[1][0]; -// return CircleAvatar( -// backgroundColor: Colors.primaries[ -// contact.displayName.hashCode % Colors.primaries.length], -// child: ReusableTextWidget( -// text: initials.toUpperCase(), -// fontSize: 16, -// fontWeight: FontWeight.bold, -// fontFamily: FontConstants.fontFamily, -// color: Colors.white, -// ), -// ); -// } -// } -// -// Future _openWhatsApp(Contact contact) async { -// if (contact.phones.isEmpty) return; -// -// String phoneNumber = -// contact.phones.first.number.replaceAll(RegExp(r'\D'), ''); -// final Uri url = Uri.parse("https://wa.me/$phoneNumber"); -// -// if (await canLaunchUrl(url)) { -// await launchUrl(url, mode: LaunchMode.externalApplication); -// } else { -// ScaffoldMessenger.of(context).showSnackBar( -// const SnackBar( -// content: ReusableTextWidget( -// text: "Could not open WhatsApp", -// fontSize: 14, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.white, -// ), -// ), -// ); -// } -// } -// -// Future _inviteWhatsApp(Contact contact) async { -// if (contact.phones.isEmpty) return; -// -// String phoneNumber = -// contact.phones.first.number.replaceAll(RegExp(r'\D'), ''); -// -// final String message = Uri.encodeComponent( -// "Hey! Join me on Nearle Daily 🚀"); -// -// final Uri url = Uri.parse("https://wa.me/$phoneNumber?text=$message"); -// -// if (await canLaunchUrl(url)) { -// await launchUrl(url, mode: LaunchMode.externalApplication); -// } else { -// ScaffoldMessenger.of(context).showSnackBar( -// const SnackBar( -// content: ReusableTextWidget( -// text: "Could not open WhatsApp", -// fontSize: 14, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.white, -// ), -// ), -// ); -// } -// } -// -// @override -// Widget build(BuildContext context) { -// return AnnotatedRegion( -// value: const SystemUiOverlayStyle( -// statusBarColor: Colors.white, // White background -// statusBarIconBrightness: Brightness.dark, // Dark icons -// statusBarBrightness: Brightness.light, // iOS -// ), -// child: Scaffold( -// backgroundColor: Colors.white, -// appBar: AppBar( -// backgroundColor: Colors.white, -// surfaceTintColor: Colors.transparent, -// scrolledUnderElevation: 0, -// titleSpacing: -5, -// animateColor: false, -// elevation: 0, -// title: ReusableTextWidget( -// text: "Refer a friend", -// fontSize: 20, -// fontWeight: FontWeight.w600, -// fontFamily: FontConstants.fontFamily, -// color: Colors.black, -// ), -// iconTheme: const IconThemeData(color: Colors.black), -// ), -// body: Padding( -// padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 12), -// child: Column( -// children: [ -// /// 🔹 MODIFIED DISCLAIMER ONLY -// if (_showDisclaimer) -// Stack( -// children: [ -// Padding( -// padding: const EdgeInsets.only(top: 12.0), -// child: Container( -// width: double.infinity, -// padding: const EdgeInsets.all(14), -// margin: const EdgeInsets.only(bottom: 16), -// decoration: BoxDecoration( -// color: ColorConstants.primaryColor.withOpacity(0.08), -// borderRadius: BorderRadius.circular(12), -// ), -// child: const ReusableTextWidget( -// text: -// "We access contacts only to let you share\nor recommend to friends. Nothing is stored.", -// fontSize: 13, -// fontWeight: FontWeight.w500, -// fontFamily: FontConstants.fontFamily, -// color: Colors.black87, -// ), -// ), -// ), -// Positioned( -// top: 6, -// right: -3, -// child: IconButton( -// icon: const Icon(Icons.close, size: 18), -// onPressed: () { -// setState(() { -// _showDisclaimer = false; -// }); -// }, -// ), -// ), -// ], -// ), -// -// if (_loading) -// const Expanded( -// child: Center(child: CircularProgressIndicator()), -// ), -// -// if (_permissionDenied) -// Expanded( -// child: Center( -// child: Container( -// margin: const EdgeInsets.symmetric(horizontal: 24), -// padding: const EdgeInsets.all(24), -// decoration: BoxDecoration( -// color: Colors.red.withOpacity(0.05), -// borderRadius: BorderRadius.circular(20), -// border: Border.all( -// color: Colors.red.withOpacity(0.2), -// ), -// ), -// child: Column( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Container( -// padding: const EdgeInsets.all(18), -// decoration: BoxDecoration( -// color: Colors.red.withOpacity(0.12), -// shape: BoxShape.circle, -// ), -// child: const Icon( -// Icons.info_outline, -// color: Colors.red, -// size: 48, -// ), -// ), -// const SizedBox(height: 20), -// const ReusableTextWidget( -// text: "Contacts Access Needed", -// fontSize: 18, -// fontWeight: FontWeight.w600, -// fontFamily: FontConstants.fontFamily, -// color: Colors.black, -// ), -// const SizedBox(height: 8), -// const ReusableTextWidget( -// text: -// "Allow contacts permission to view\nand invite your friends easily.", -// fontSize: 14, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.black54, -// textAlign: TextAlign.center, -// ), -// const SizedBox(height: 24), -// SizedBox( -// width: double.infinity, -// child: ElevatedButton( -// onPressed: permission_handler.openAppSettings, -// style: ElevatedButton.styleFrom( -// backgroundColor: Colors.red, -// elevation: 0, -// padding: const EdgeInsets.symmetric(vertical: 14), -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular(12), -// ), -// ), -// child: const ReusableTextWidget( -// text: "Open Settings", -// fontSize: 15, -// fontWeight: FontWeight.w600, -// fontFamily: FontConstants.fontFamily, -// color: Colors.white, -// ), -// ), -// ), -// ], -// ), -// ), -// ), -// ), -// -// if (_contacts.isNotEmpty && !_loading && !_permissionDenied) -// Expanded( -// child: RefreshIndicator( -// onRefresh: _loadContacts, -// child: ListView.builder( -// itemCount: _contacts.length, -// itemBuilder: (context, index) { -// final contact = _contacts[index]; -// final phones = -// contact.phones.map((p) => p.number).toList(); -// final subtitle = phones.length > 1 -// ? phones.sublist(0, 2).join(", ") -// : phones.first; -// -// return ListTile( -// leading: _buildAvatar(contact), -// title: ReusableTextWidget( -// text: contact.displayName.isEmpty -// ? "No Name" -// : contact.displayName, -// fontSize: 14, -// fontWeight: FontWeight.w600, -// fontFamily: FontConstants.fontFamily, -// color: Colors.black, -// ), -// subtitle: ReusableTextWidget( -// text: subtitle, -// fontSize: 13, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.grey, -// ), -// trailing: TextButton( -// onPressed: () => _inviteWhatsApp(contact), -// child: const ReusableTextWidget( -// text: "Invite", -// fontSize: 14, -// fontWeight: FontWeight.bold, -// fontFamily: FontConstants.fontFamily, -// color: Colors.green, -// ), -// ), -// onTap: () => _openWhatsApp(contact), -// ); -// }, -// ), -// ), -// ), -// -// if (_contacts.isEmpty && !_loading && !_permissionDenied) -// const Expanded( -// child: Center( -// child: ReusableTextWidget( -// text: "No contacts found with phone numbers", -// fontSize: 16, -// fontWeight: FontWeight.w400, -// fontFamily: FontConstants.fontFamily, -// color: Colors.grey, -// ), -// ), -// ), -// ], -// ), -// ), -// ), -// ); -// } -// } +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../constants/color_constants.dart'; +import '../../constants/font_constants.dart'; +import '../../widgets/text_widget.dart'; + +// ───────────────────────────────────────────────────────────────────────────── +// ReferEarnScreen (StatefulWidget) +// ───────────────────────────────────────────────────────────────────────────── + +class ReferEarnScreen extends StatefulWidget { + const ReferEarnScreen({super.key}); + + @override + State createState() => _ReferEarnScreenState(); +} + +class _ReferEarnScreenState extends State { + bool _codeCopied = false; + + static const String _referralCode = 'ANBU123'; + static const int _totalCoins = 1250; + + + // ── helpers ──────────────────────────────────────────────────────────────── + void _copyCode() async { + await Clipboard.setData(const ClipboardData(text: _referralCode)); + setState(() => _codeCopied = true); + await Future.delayed(const Duration(seconds: 2)); + if (mounted) setState(() => _codeCopied = false); + } + + + // ── build ────────────────────────────────────────────────────────────────── + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: true, + child: Scaffold( + backgroundColor: Color(0xFFF6F6F6), + appBar: AppBar( + + surfaceTintColor: Colors.transparent, + + // 🔥 Prevent color overlay when scrolled + scrolledUnderElevation: 0, + animateColor: false, // ✨ prevent color change on scroll + elevation: 0, + backgroundColor: Colors.white, + + leading: const BackButton(color: Color(0xFF1A1A2E)), + + title: ReusableTextWidget( + text: 'Refer & Earn', + color: const Color(0xFF1A1A2E), + fontFamily: FontConstants.fontFamily, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTotalCoinsBanner(), + const SizedBox(height: 16), + _buildReferralCodeCard(), + const SizedBox(height: 20), + + Center( + child: ReusableTextWidget( + text: 'How It Works', + color: const Color(0xFF1A1A2E), + fontFamily: FontConstants.fontFamily, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 16), + _buildHowItWorksRow(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + // ── Total Coins Banner ───────────────────────────────────────────────────── + + Widget _buildTotalCoinsBanner() { + + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + ColorConstants.primaryColor.withOpacity(0.85), + ColorConstants.primaryColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: 'Total Coins', + color: Colors.white, + fontFamily: FontConstants.fontFamily, + fontSize: 13, + fontWeight: FontWeight.w800, + ), + const SizedBox(height: 4), + ReusableTextWidget( + text: '$_totalCoins Coins', + color: Colors.white, + fontFamily: FontConstants.fontFamily, + fontSize: 28, + fontWeight: FontWeight.w800, + ), + const SizedBox(height: 4), + ReusableTextWidget( + text: 'Earn more by referring friends', + color: Colors.white60, + fontFamily: FontConstants.fontFamily, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ], + ), + ), + Container( + width: 80, + height: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white.withOpacity(0.15), + ), + child: const Center( + child: Text('🪙', style: TextStyle(fontSize: 36)), + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ); + } + + // ── Referral Code Card ───────────────────────────────────────────────────── + + Widget _buildReferralCodeCard() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: 'Your Referral Code', + color: Colors.black.withOpacity(0.7), + fontFamily: FontConstants.fontFamily, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black.withOpacity(0.7), + width: 1.5), + borderRadius: BorderRadius.circular(10), + ), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ReusableTextWidget( + text: _referralCode, + color: Colors.black.withOpacity(0.7), + fontFamily: FontConstants.fontFamily, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + GestureDetector( + onTap: _copyCode, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _codeCopied + ? Row( + key: const ValueKey('copied'), + children: [ + const Icon(Icons.check_circle, + size: 18, color: Color(0xFF2ECC71)), + const SizedBox(width: 4), + ReusableTextWidget( + text: 'Copied!', + color: const Color(0xFF2ECC71), + fontFamily: FontConstants.fontFamily, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ], + ) + : Row( + key: const ValueKey('copy'), + children: [ + Icon(Icons.copy, + size: 18, + color: ColorConstants.primaryColor), + const SizedBox(width: 4), + ReusableTextWidget( + text: 'Copy', + color: ColorConstants.primaryColor, + fontFamily: FontConstants.fontFamily, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.share_rounded, color: Colors.white), + label: ReusableTextWidget( + text: 'Invite Friends & Earn', + color: Colors.white, + fontFamily: FontConstants.fontFamily, + fontSize: 15, + fontWeight: FontWeight.w700, + ), + style: ElevatedButton.styleFrom( + backgroundColor: ColorConstants.primaryColor, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ), + const SizedBox(height: 10), + Center( + child: ReusableTextWidget( + text: 'Share your code or link with friends', + color: Colors.grey, + fontFamily: FontConstants.fontFamily, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + + // ── How It Works ─────────────────────────────────────────────────────────── + + Widget _buildHowItWorksRow() { + final steps = [ + { + 'step': 1, + 'icon': Icons.share_rounded, + 'label': 'Share', + 'color': ColorConstants.primaryColor, + }, + { + 'step': 2, + 'icon': Icons.download_rounded, + 'label': 'Install', + 'color': const Color(0xFF3498DB), + }, + { + 'step': 3, + 'icon': Icons.how_to_reg_rounded, + 'label': 'Register', + 'color': const Color(0xFF2ECC71), + }, + { + 'step': 4, + 'icon': Icons.card_giftcard_rounded, + 'label': 'Reward', + 'color': const Color(0xFFE67E22), + }, + ]; + + return Row( + children: steps.map((s) { + final color = s['color'] as Color; + + return Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 4, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: color.withOpacity(0.12), + shape: BoxShape.circle, + ), + child: Icon( + s['icon'] as IconData, + color: color, + size: 20, + ), + ), + Positioned( + top: -3, + left: -3, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${s['step']}', + style: const TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ReusableTextWidget( + text: s['label'] as String, + color: const Color(0xFF555555), + fontFamily: FontConstants.fontFamily, + fontSize: 9, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }).toList(), + ); + } + +} \ No newline at end of file diff --git a/lib/view/authentication/costomer_create_view.dart b/lib/view/authentication/costomer_create_view.dart index c918d55..295f6c6 100644 --- a/lib/view/authentication/costomer_create_view.dart +++ b/lib/view/authentication/costomer_create_view.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -13,7 +14,8 @@ import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../../constants/color_constants.dart'; import '../../constants/font_constants.dart'; -import '../../controllers/tenant_controller /tenant_list.dart'; +import '../../controllers/tenant_list/tenant_controller.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; import '../../widgets/text_widget.dart'; import '../home_view.dart'; @@ -114,6 +116,19 @@ class _CustomerCreateViewState extends State { await prefs.setString('customerDoorNo', details['doorno'] ?? ''); debugPrint("✅ Customer info saved to SharedPreferences."); + + await FirebaseAnalytics.instance.setUserId( + id: customerIdStr, + ); + + await AnalyticsService.logEvent( + 'signup_success', + parameters: { + 'customer_id': int.tryParse(customerIdStr) ?? 0, + 'customer_name': details['firstname'] ?? '', + 'user_type': 'new_user', + }, + ); } tenantController.loadTenants(); @@ -131,11 +146,7 @@ class _CustomerCreateViewState extends State { fontSize: 15, ); - - } else { - - // ❌ Handle failure message from API debugPrint("❌ API returned failure: $message"); @@ -147,8 +158,6 @@ class _CustomerCreateViewState extends State { textColor: Colors.white, fontSize: 15, ); - - } } catch (e, stacktrace) { debugPrint(" Something went wrong"); @@ -161,17 +170,11 @@ class _CustomerCreateViewState extends State { textColor: Colors.white, fontSize: 15, ); - - } } @override Widget build(BuildContext context) { - - - - final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; diff --git a/lib/view/authentication/login_view.dart b/lib/view/authentication/login_view.dart index 4486f9b..3842346 100644 --- a/lib/view/authentication/login_view.dart +++ b/lib/view/authentication/login_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../controllers/authentication/auth_controller.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; import '../authentication/verification_view.dart'; class Login_view extends StatelessWidget { @@ -319,8 +320,13 @@ class Login_view extends StatelessWidget { elevation: 0, ), onPressed: canProceed - ? () => - authController.signIn(context, phone) + ? () async { + await AnalyticsService.logEvent( + 'login_continue_clicked', + ); + + authController.signIn(context, phone); + } : null, child: authController.isLoading.value ? const SizedBox( diff --git a/lib/view/cart/cart_view.dart b/lib/view/cart/cart_view.dart index a84412b..13b6e16 100644 --- a/lib/view/cart/cart_view.dart +++ b/lib/view/cart/cart_view.dart @@ -347,13 +347,39 @@ class _CartPageState extends State { const Padding( padding: EdgeInsets.all(6.0), child: Text( - 'No slots available for today.\nPlease try changing dates to schedule orders.', + 'Your cart is empty.\nBrowse our products to get started.', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey, fontWeight: FontWeight.bold), ), ), + const SizedBox(height: 24), + SizedBox( + width: 200, + height: 46, + child: ElevatedButton( + onPressed: () { + // Switch to the Stores tab (index 1) via the shared GetX controller + Get.find().currentIndex.value = 1; + }, + style: ElevatedButton.styleFrom( + backgroundColor: ColorConstants.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: const Text( + 'Browse Products', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ), ], ), ) diff --git a/lib/view/cart/order_countdown_page.dart b/lib/view/cart/order_countdown_page.dart index 5aff41c..2018933 100644 --- a/lib/view/cart/order_countdown_page.dart +++ b/lib/view/cart/order_countdown_page.dart @@ -6,6 +6,7 @@ import '../../constants/font_constants.dart'; import '../../controllers/cart_controller/cart.dart'; import '../../controllers/order_controller/create_order_controller.dart'; import '../../modules/orders/create_order.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; import '../../widgets/text_widget.dart'; import '../orders/order_succes.dart'; @@ -99,12 +100,25 @@ class _OrderCountdownPageState extends State if (!widget.orderCtrl.isLoading.value) { + + await AnalyticsService.logEvent( + 'order_placed', + parameters: { + 'customer_id': widget.order.customerid ?? 0, + 'tenant_id': widget.order.tenantid ?? 0, + 'item_count': widget.order.items?.length ?? 0, + 'payment_type': widget.order.paymenttype ?? 0, + }, + ); + Get.offAll(() => OrderSuccessView()); widget.cartCtrl.clearCart(); await widget.cartCtrl.notifyAdmin( title: 'Nearle Deals - New Order', body: 'A new order has been placed successfully by ${widget.customerName}!', ); + print('jeee'); + print(widget.order.toJson()); } } diff --git a/lib/view/dashboard_view/ai_search.dart b/lib/view/dashboard_view/ai_search.dart new file mode 100644 index 0000000..c5af44a --- /dev/null +++ b/lib/view/dashboard_view/ai_search.dart @@ -0,0 +1,1625 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import 'package:lottie/lottie.dart'; + +import '../../modules/orders/create_order.dart'; + +// ───────────────────────────────────────────────────────────────────────────── +// Shimmer widget +// ───────────────────────────────────────────────────────────────────────────── + +class _ShimmerBox extends StatefulWidget { + final double width; + final double height; + final BorderRadius borderRadius; + + const _ShimmerBox({ + required this.width, + required this.height, + this.borderRadius = const BorderRadius.all(Radius.circular(8)), + }); + + @override + State<_ShimmerBox> createState() => _ShimmerBoxState(); +} + +class _ShimmerBoxState extends State<_ShimmerBox> + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + late Animation _anim; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); + _anim = Tween(begin: -1.5, end: 2.5).animate( + CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _anim, + builder: (_, __) { + return Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: widget.borderRadius, + gradient: LinearGradient( + begin: Alignment(_anim.value - 1, 0), + end: Alignment(_anim.value, 0), + colors: const [ + Color(0xFFE8E8E8), + Color(0xFFF5F5F5), + Color(0xFFE8E8E8), + ], + ), + ), + ); + }, + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SearchPage +// ───────────────────────────────────────────────────────────────────────────── + +class SearchPage extends StatefulWidget { + const SearchPage({super.key}); + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + final TextEditingController searchController = TextEditingController(); + + bool isLoading = false; + Map? responseData; + String? errorMessage; + String? clarificationQuestion; // ← NEW: holds question when needs_clarification + + File? selectedImage; + List clarificationOptions = []; + + Future searchIngredients() async { + final query = searchController.text.trim(); + if (query.isEmpty) return; + + setState(() { + isLoading = true; + errorMessage = null; + responseData = null; + clarificationQuestion = null; // reset on every new search + }); + + try { + final response = await http + .post( + Uri.parse('http://127.0.0.1:8000/find-ingredients'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + "user_input": query, + "customer_id": 6060, + "latitude": 11.0050728, + "longitude": 76.9508513, + }), + ) + .timeout(const Duration(seconds: 15)); + + if (!mounted) return; + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + + if (decoded is Map) { + final status = decoded['status']?.toString(); + + if (status == 'needs_clarification') { + setState(() { + clarificationQuestion = decoded['question']?.toString(); + + clarificationOptions = + (decoded['options'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; + }); + } else { + setState(() { + responseData = decoded; + clarificationOptions = []; + }); + } + } else { + setState(() { + errorMessage = 'Unexpected response format.'; + }); + } + } else { + setState(() { + errorMessage = 'Server error (${response.statusCode}). Try again.'; + }); + debugPrint("API Error: ${response.body}"); + } + } catch (e) { + if (!mounted) return; + setState(() { + errorMessage = 'Could not connect. Check your network and try again.'; + }); + debugPrint("Error: $e"); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); + } + + Future pickImage() async { + final XFile? image = await ImagePicker().pickImage( + source: ImageSource.gallery, + imageQuality: 80, + ); + + if (image != null) { + setState(() { + selectedImage = File(image.path); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + surfaceTintColor: Colors.transparent, + scrolledUnderElevation: 0, + elevation: 0, + backgroundColor: Colors.white, + leading: Row( + children: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back), + ), + const Text( + "Search", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ], + ), + leadingWidth: 300, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Search bar ── + Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.06), + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 6), + ), + ], + border: Border.all(color: Colors.grey.shade300, width: 0.5), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: searchController, + decoration: const InputDecoration( + hintText: "Search ingredients...", + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + onSubmitted: (_) => searchIngredients(), + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.mic), + ), + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text("Camera"), + onTap: () => Navigator.pop(context), + ), + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text("Gallery"), + onTap: () { + Navigator.pop(context); + pickImage(); + }, + ), + ], + ), + ); + }, + icon: const Icon(Icons.camera_alt_outlined), + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: isLoading + ? SizedBox( + width: 50, + height: 50, + child: Lottie.asset('assets/lotties/ai_loader.json'), + ) + : ElevatedButton( + onPressed: searchIngredients, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF662582), + elevation: 0, + shape: const CircleBorder(), + padding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + ), + child: const Icon(Icons.search, color: Colors.white), + ), + ), + ], + ), + ), + if (clarificationQuestion != null) ...[ + const SizedBox(height: 12), + + Text( + clarificationQuestion!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + + const SizedBox(height: 10), + ], + if (clarificationOptions.isNotEmpty) + Wrap( + spacing: 8, + runSpacing: 8, + children: clarificationOptions.map((option) { + return ActionChip( + label: Text(option), + onPressed: () { + searchController.text = option; + // Call API again with selected option + searchIngredients(); + }, + ); + }).toList(), + ), + ], + ), + + const SizedBox(height: 20), + + // ── Results area ── + Expanded( + child: isLoading + ? _buildShimmer() + : errorMessage != null + ? _buildError() + : clarificationQuestion != null // ← NEW: check clarification first + ? _buildClarification() // ← NEW: show agent question + : responseData == null + ? const Center(child: Text("Search for ingredients")) + : _buildResults(), + ), + ], + ), + ), + ); + } + + // ── Shimmer skeleton ── + Widget _buildShimmer() { + return ListView( + physics: const NeverScrollableScrollPhysics(), + children: [ + const _ShimmerBox(width: 160, height: 22), + const SizedBox(height: 12), + SizedBox( + height: 150, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (_, __) => const _ShimmerBox( + width: 130, + height: 150, + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + ), + const SizedBox(height: 24), + const _ShimmerBox(width: 140, height: 22), + const SizedBox(height: 12), + SizedBox( + height: 176, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (_, __) => const _ShimmerBox( + width: 250, + height: 176, + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + ), + ), + ], + ); + } + + // ── Error state ── + Widget _buildError() { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.wifi_off_rounded, size: 48, color: Colors.grey), + const SizedBox(height: 12), + Text( + errorMessage!, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: searchIngredients, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + + // ── NEW: Clarification card (only shown when status == needs_clarification) ── + Widget _buildClarification() { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFF3E5F5), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFF662582).withOpacity(0.25), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF662582).withOpacity(0.06), + blurRadius: 20, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + // AI avatar icon + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: const Color(0xFF662582).withOpacity(0.12), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.auto_awesome_rounded, + size: 26, + color: Color(0xFF662582), + ), + ), + const SizedBox(height: 14), + + // "AI Assistant" label + const Text( + 'AI Assistant', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF662582), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + + // The actual question from the agent + Text( + clarificationQuestion ?? 'Can you clarify your request?', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + height: 1.4, + ), + ), + const SizedBox(height: 12), + + // Hint + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.6), + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.keyboard_rounded, + size: 13, color: Color(0xFF757575)), + SizedBox(width: 5), + Text( + 'Type your answer above and search again', + style: TextStyle( + fontSize: 11, color: Color(0xFF757575)), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // ── Normal results ── + Widget _buildResults() { + final ingredients = (responseData!['ingredients_needed'] as List?) ?? []; + + return ListView( + children: [ + const Text( + "Recommended", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 12), + + if (ingredients.isEmpty) + const Text('No ingredients found.', + style: TextStyle(color: Colors.grey)) + else + SizedBox( + height: 150, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: ingredients.length, + itemBuilder: (context, index) { + final item = + ingredients[index] as Map? ?? {}; + + return Container( + width: 130, + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.grey.shade300, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + (item['emoji'] as String?)?.isNotEmpty == true + ? item['emoji'] as String + : '🥬', + style: const TextStyle(fontSize: 35), + ), + const SizedBox(height: 8), + Text( + item['name']?.toString() ?? '', + textAlign: TextAlign.center, + maxLines: 2, + style: const TextStyle( + fontSize: 12, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + item['quantity']?.toString() ?? '', + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold), + ), + ], + ), + ); + }, + ), + ), + + const SizedBox(height: 24), + + CompareStoresSection( + responseData: responseData, + onStoreSelected: (storeData) { + debugPrint( + 'Selected: ${(storeData['store'] as Map?)?['name']}'); + }, + ), + + const SizedBox(height: 24), + ], + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// StoreCompareCard +// ───────────────────────────────────────────────────────────────────────────── + +class StoreCompareCard extends StatelessWidget { + final String storeName; + final String location; + final double rating; + final int reviewCount; + final String discountLabel; + final double originalPrice; + final double discountedPrice; + final double savings; + final int matchCount; + final int totalIngredients; + final String? imageUrl; + final bool isBestValue; + final bool isSelected; + final VoidCallback? onSelectStore; + + const StoreCompareCard({ + super.key, + required this.storeName, + required this.location, + required this.rating, + required this.reviewCount, + required this.discountLabel, + required this.originalPrice, + required this.discountedPrice, + required this.savings, + required this.matchCount, + required this.totalIngredients, + this.imageUrl, + this.isBestValue = false, + this.isSelected = false, + this.onSelectStore, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 250, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + isSelected ? const Color(0xFF2E7D32) : Colors.grey.shade300, + width: 1, + ), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: imageUrl != null && imageUrl!.isNotEmpty + ? Image.network( + imageUrl!, + width: 40, + height: 45, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + _placeholderIcon(), + ) + : _placeholderIcon(), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + storeName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + height: 1.2, + ), + ), + const SizedBox(height: 4), + if (rating > 0) + Row( + children: [ + const Icon(Icons.star_rounded, + size: 11, color: Color(0xFFFFA000)), + const SizedBox(width: 2), + Flexible( + child: Text( + '$rating (${_formatCount(reviewCount)})', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF616161)), + ), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + const Icon(Icons.location_on_outlined, + size: 11, color: Color(0xFF616161)), + const SizedBox(width: 2), + Flexible( + child: Text( + location, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 8, + color: Color(0xFF616161)), + ), + ), + ], + ), + if (discountLabel.isNotEmpty) + Container( + margin: const EdgeInsets.only(top: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + discountLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 7, + fontWeight: FontWeight.w700, + color: Color(0xFF2E7D32), + ), + ), + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + '₹${_fmt(originalPrice)}', + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + decoration: TextDecoration + .lineThrough, + decorationColor: Colors.grey, + ), + ), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + '₹${_fmt(discountedPrice)}', + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w800, + color: Color(0xFF2E7D32), + ), + ), + ), + ], + ), + const SizedBox(height: 2), + Text( + 'You save ₹${_fmt(savings)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF2E7D32)), + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + '$matchCount / $totalIngredients', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const Text('items', + style: TextStyle( + fontSize: 7, + color: Color(0xFF757575))), + const Text('available', + style: TextStyle( + fontSize: 7, + color: Color(0xFF757575))), + ], + ), + ], + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 7), + + Align( + alignment: Alignment.center, + child: InkWell( + onTap: onSelectStore, + borderRadius: BorderRadius.circular(2), + highlightColor: Colors.transparent, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF2E7D32) + : Colors.white, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isSelected + ? const Color(0xFF2E7D32) + : Colors.grey.shade300, + width: 1, + ), + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: Text( + isSelected ? 'Selected ✓' : 'Select Store', + key: ValueKey(isSelected), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? Colors.white + : const Color(0xFF2E7D32), + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + ), + ], + ), + ), + + if (isBestValue) + Positioned( + top: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: const BoxDecoration( + color: Color(0xFF2E7D32), + borderRadius: BorderRadius.only( + topRight: Radius.circular(14), + bottomLeft: Radius.circular(10), + ), + ), + child: const Text( + 'Best Value', + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _placeholderIcon() => Container( + width: 45, + height: 50, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.store_rounded, + size: 20, color: Colors.grey.shade400), + ); + + String _fmt(double v) => v == v.truncateToDouble() + ? v.toInt().toString() + : v.toStringAsFixed(2); + + String _formatCount(int count) => + count >= 1000 ? '${(count / 1000).toStringAsFixed(1)}k' : '$count'; +} + +// ───────────────────────────────────────────────────────────────────────────── +// CompareStoresSection +// ───────────────────────────────────────────────────────────────────────────── + +class CompareStoresSection extends StatefulWidget { + final Map? responseData; + final void Function(Map storeData)? onStoreSelected; + + const CompareStoresSection({ + super.key, + required this.responseData, + this.onStoreSelected, + }); + + @override + State createState() => _CompareStoresSectionState(); +} + +class _CompareStoresSectionState extends State { + int _selectedIndex = 0; + + List get _stores => + (widget.responseData?['stores'] as List?) ?? []; + + String _discountLabel( + Map store, Map storeData) { + final label = store['discount_label'] ?? storeData['discount_label']; + if (label != null && label.toString().isNotEmpty) { + return label.toString(); + } + final orig = _toDouble( + store['total_original_price'] ?? storeData['total_original_price']); + final disc = _toDouble(store['total_discounted_price'] ?? + storeData['total_discounted_price']); + if (orig > 0 && disc < orig) { + final pct = ((orig - disc) / orig * 100).toStringAsFixed(1); + return '$pct% OFF'; + } + return ''; + } + + double _toDouble(dynamic v) => + v == null ? 0.0 : double.tryParse(v.toString()) ?? 0.0; + + int _toInt(dynamic v) => + v == null ? 0 : int.tryParse(v.toString()) ?? 0; + + @override + Widget build(BuildContext context) { + if (_stores.isEmpty) return const SizedBox.shrink(); + + final selectedStore = _stores[_selectedIndex]; + final matchedProducts = + (selectedStore['matched_products'] as List?) ?? []; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Compare stores", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A)), + ), + Text( + '${_stores.length} stores found', + style: + const TextStyle(fontSize: 13, color: Color(0xFF757575)), + ), + ], + ), + + const SizedBox(height: 12), + + SizedBox( + height: 163, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _stores.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final raw = _stores[index]; + if (raw is! Map) { + return const SizedBox.shrink(); + } + + final storeData = raw; + final store = + (storeData['store'] as Map?) ?? {}; + + final isBestValue = storeData['is_best_value'] == true || + (storeData['is_best_value'] == null && index == 0); + + return StoreCompareCard( + storeName: + store['tenantname']?.toString().isNotEmpty == true + ? store['tenantname'].toString() + : 'Unknown Store', + location: store['locationname']?.toString() ?? '', + rating: _toDouble(store['rating']), + reviewCount: _toInt(store['review_count']), + discountLabel: _discountLabel(store, storeData), + originalPrice: _toDouble(store['total_original_price'] ?? + storeData['total_original_price']), + discountedPrice: _toDouble( + store['total_discounted_price'] ?? + storeData['total_discounted_price']), + savings: _toDouble(store['total_savings'] ?? + storeData['total_savings']), + matchCount: _toInt(storeData['match_count']), + totalIngredients: _toInt(storeData['total_ingredients']), + imageUrl: store['image_url']?.toString(), + isBestValue: isBestValue, + isSelected: _selectedIndex == index, + onSelectStore: () { + setState(() => _selectedIndex = index); + widget.onStoreSelected?.call(storeData); + }, + ); + }, + ), + ), + + if (matchedProducts.isNotEmpty) ...[ + const SizedBox(height: 24), + _SelectedStoreProducts( + key: ValueKey(_selectedIndex), + matchedProducts: matchedProducts, + ), + ], + ], + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// _SelectedStoreProducts +// ───────────────────────────────────────────────────────────────────────────── + +class _SelectedStoreProducts extends StatefulWidget { + final List matchedProducts; + + const _SelectedStoreProducts({ + super.key, + required this.matchedProducts, + }); + + @override + State<_SelectedStoreProducts> createState() => + _SelectedStoreProductsState(); +} + +class _SelectedStoreProductsState extends State<_SelectedStoreProducts> { + final Map _cartQuantities = {}; + bool _isPlacingOrder = false; + + @override + void initState() { + super.initState(); + _initCart(); + } + + void _initCart() { + for (final item in widget.matchedProducts) { + if (item is! Map) continue; + final product = (item['product'] as Map?) ?? {}; + final productId = product['productid'] as int?; + if (productId != null) { + _cartQuantities[productId] = 1; + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Products", + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A)), + ), + const SizedBox(height: 12), + + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.matchedProducts.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final item = widget.matchedProducts[index]; + if (item is! Map) { + return const SizedBox.shrink(); + } + + final product = + (item['product'] as Map?) ?? {}; + final productId = product['productid'] as int? ?? index; + final qty = _cartQuantities[productId] ?? 0; + + final imageUrl = product['productimage']?.toString() ?? ''; + final name = product['productname']?.toString() ?? + item['ingredient']?.toString() ?? + ''; + final unit = product['productunit']?.toString() ?? ''; + final originalPrice = double.tryParse( + product['original_price']?.toString() ?? '') ?? + 0.0; + final discountedPrice = double.tryParse( + product['discounted_price']?.toString() ?? '') ?? + 0.0; + final discountPct = double.tryParse( + product['discount_percent']?.toString() ?? '') ?? + 0.0; + final hasOffer = product['has_offer'] == true; + final emoji = item['emoji']?.toString() ?? ''; + final quantity = item['quantity']?.toString() ?? ''; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: Colors.grey.shade200, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: imageUrl.isNotEmpty + ? Image.network( + imageUrl, + width: 64, + height: 64, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + _productPlaceholder(emoji), + ) + : _productPlaceholder(emoji), + ), + + const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 2), + Text( + '$quantity · $unit', + style: const TextStyle( + fontSize: 11, color: Color(0xFF757575)), + ), + const SizedBox(height: 6), + Row( + children: [ + if (hasOffer && + originalPrice > discountedPrice) ...[ + Text( + '₹${_fmt(originalPrice)}', + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + decoration: TextDecoration.lineThrough, + decorationColor: Colors.grey, + ), + ), + const SizedBox(width: 6), + ], + Text( + '₹${_fmt(discountedPrice)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + color: Color(0xFF2E7D32), + ), + ), + if (hasOffer && discountPct > 0) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${discountPct.toStringAsFixed(1)}% OFF', + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: Color(0xFF2E7D32), + ), + ), + ), + ], + ], + ), + ], + ), + ), + + const SizedBox(width: 8), + + qty == 0 + ? OutlinedButton( + onPressed: () => setState( + () => _cartQuantities[productId] = 1), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF662582), + side: const BorderSide( + color: Color(0xFF662582), width: 1.5), + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + 'Add', + style: TextStyle( + fontSize: 13, fontWeight: FontWeight.w700), + ), + ) + : Container( + decoration: BoxDecoration( + color: const Color(0xFF662582), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _stepperBtn( + icon: Icons.remove, + onTap: () => setState(() { + if (qty <= 1) { + _cartQuantities.remove(productId); + } else { + _cartQuantities[productId] = qty - 1; + } + }), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10), + child: Text( + '$qty', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + _stepperBtn( + icon: Icons.add, + onTap: () => setState(() => + _cartQuantities[productId] = qty + 1), + ), + ], + ), + ), + ], + ), + ); + }, + ), + + const SizedBox(height: 20), + + _buildOrderSummary(), + + const SizedBox(height: 12), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: (_cartQuantities.isEmpty || _isPlacingOrder) + ? null + : _placeOrder, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF662582), + disabledBackgroundColor: Colors.grey.shade300, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + child: _isPlacingOrder + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + 'Place Order', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700), + ), + ), + ), + + const SizedBox(height: 8), + ], + ); + } + + Widget _buildOrderSummary() { + double total = 0; + int itemCount = 0; + + for (final item in widget.matchedProducts) { + if (item is! Map) continue; + final product = (item['product'] as Map?) ?? {}; + final productId = product['productid'] as int?; + final qty = + productId != null ? (_cartQuantities[productId] ?? 0) : 0; + if (qty > 0) { + final price = double.tryParse( + product['discounted_price']?.toString() ?? '') ?? + 0.0; + total += price * qty; + itemCount += qty; + } + } + + if (itemCount == 0) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$itemCount item${itemCount > 1 ? 's' : ''} in cart', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF2E7D32)), + ), + Text( + '₹${_fmt(total)}', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: Color(0xFF2E7D32)), + ), + ], + ), + ); + } + + Future _placeOrder() async { + if (_cartQuantities.isEmpty || _isPlacingOrder) return; + + setState(() => _isPlacingOrder = true); + + try { + final firstItem = + widget.matchedProducts.first as Map; + final firstProduct = + (firstItem['product'] as Map?) ?? {}; + + final tenantid = firstProduct['tenantid'] as int? ?? 0; + final categoryid = firstProduct['categoryid'] as int? ?? 0; + final subcategoryid = firstProduct['subcategoryid'] as int? ?? 0; + + final items = []; + double total = 0; + + for (final item in widget.matchedProducts) { + if (item is! Map) continue; + final product = (item['product'] as Map?) ?? {}; + final productId = product['productid'] as int?; + if (productId == null) continue; + + final qty = _cartQuantities[productId] ?? 0; + if (qty <= 0) continue; + + final cost = (product['productcost'] as num?)?.toDouble() ?? 0; + final discountedPrice = double.tryParse( + product['discounted_price']?.toString() ?? '') ?? + 0.0; + final tax = (product['taxamount'] as num?)?.toDouble() ?? 0; + final discount = + double.tryParse(product['you_save']?.toString() ?? '') ?? 0.0; + + final lineTotal = discountedPrice * qty; + total += lineTotal; + + items.add( + OrderItem( + productid: productId, + productname: product['productname']?.toString() ?? '', + productdescription: + product['productdesc']?.toString() ?? '', + orderqty: qty, + price: cost, + unitid: int.tryParse( + product['unitvalue']?.toString() ?? '') ?? + 1, + unitname: product['productunit']?.toString() ?? 'unit', + productsumprice: lineTotal, + tax: tax, + discount: discount, + tenantfee: 0, + ), + ); + } + + if (items.isEmpty) return; + + final now = DateTime.now(); + final orderDateStr = _fmtNow(now); + final deliveryDateStr = + '${_fmtDate(now)} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:00'; + + final order = CreateOrder( + applocationid: 1, + applocation: '', + tenantid: tenantid, + partnerid: 60, + locationid: 1135, + categoryid: categoryid, + subcategoryid: subcategoryid, + moduleid: 2, + configid: 1, + orderdate: orderDateStr, + deliverydate: deliveryDateStr, + orderstatus: 'created', + deliverycharge: 0, + customerid: 6060, + pickupcustomer: 'Ragul Stores', + pickupcontactno: '7402223869', + pickupaddress: + '412-419, R.S. Puram,Coimbatore,Tamil Nadu ,India,641002.', + pickuplocationid: 1135, + pickupcity: 'Coimbatore', + deliverycustomer: 'Test User', + deliverycontactno: '9999999999', + deliveryaddress: 'Test delivery address', + deliverylocationid: 0, + deliverylat: '11.0050728', + deliverylong: '76.9508513', + paymenttype: 42, + items: items, + ); + + final response = await http + .post( + Uri.parse( + 'https://queue.workolik.com/live/api/v1/mob/orders/createorder'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(order.toJson()), + ) + .timeout(const Duration(seconds: 15)); + + if (response.statusCode == 200 || response.statusCode == 201) { + debugPrint('Order placed successfully: ${response.body}'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order placed successfully!')), + ); + } + } else { + debugPrint( + 'Order failed: ${response.statusCode} ${response.body}'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Order failed (${response.statusCode}).')), + ); + } + } + } catch (e) { + debugPrint('Order error: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Could not place order. Try again.')), + ); + } + } finally { + if (mounted) setState(() => _isPlacingOrder = false); + } + } + + String _fmtNow(DateTime dt) => + '${_fmtDate(dt)} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + + String _fmtDate(DateTime dt) => + '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}'; + + Widget _stepperBtn( + {required IconData icon, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Icon(icon, color: Colors.white, size: 16), + ), + ); + } + + Widget _productPlaceholder(String emoji) => Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + emoji.isNotEmpty ? emoji : '🛒', + style: const TextStyle(fontSize: 28), + ), + ), + ); + + String _fmt(double v) => v == v.truncateToDouble() + ? v.toInt().toString() + : v.toStringAsFixed(2); +} \ No newline at end of file diff --git a/lib/view/dashboard_view/dashboard_view.dart b/lib/view/dashboard_view/dashboard_view.dart index ec07598..a0a9fe3 100644 --- a/lib/view/dashboard_view/dashboard_view.dart +++ b/lib/view/dashboard_view/dashboard_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:animations/animations.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/cupertino.dart'; @@ -27,8 +28,12 @@ import '../../controllers/cart_controller/cart.dart'; import '../../controllers/dashboard_controller/category.dart'; import '../../controllers/dashboard_controller/dashboard_controller.dart'; import '../../controllers/product/product_controller.dart'; -import '../../controllers/tenant_controller /tenant_list.dart'; +import '../../controllers/tenant_list/tenant_controller.dart'; import '../../domain/repository/authentication/auth_repository.dart'; +import '../../features/Products/product_card.dart'; +import '../../modules/search_model/search_model.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; +import '../../widgets/fav_button.dart'; import '../../widgets/tenantcategory.dart'; import '../../widgets/text_widget.dart'; import '../account/demo.dart'; @@ -37,8 +42,10 @@ import '../cart/cart_view.dart'; import '../home_view.dart'; import '../product/category_products.dart'; import '../product/product_view.dart'; +import '../product/products_list.dart' hide ProductItem; import '../product/tenant_products.dart'; import '../qr_scaner/qr_scaner.dart'; +import 'ai_search.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({super.key}); @@ -72,6 +79,87 @@ class _DashboardPageState extends State { static const _kProfile = 'cached_profile'; + final recommendations = [ + RecommendedItem( + title: 'Cheapest Pizza\nNearby', + subtitle: 'From ₹149', + image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS9b5A0dGeZ4PR6cdpuvDyoMa3lO7JBRyMo8Q&s', + bgColor: Color(0xFFFFF3EC), + accentColor: Color(0xFFFF6B2C), + ), + RecommendedItem( + title: 'Grocery Basket\n₹320 Less', + subtitle: 'Save more on your order', + image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=200&fit=crop', + bgColor: Color(0xFFF0FAF0), + accentColor: Color(0xFF22A45D), + ), + RecommendedItem( + title: 'Birthday\nComing Up!', + subtitle: 'Let Nearle plan it', + image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&fit=crop', + bgColor: Color(0xFFFDF0F8), + accentColor: Color(0xFFD63384), + ), + ]; + + final products = [ + ProductItem( + name: 'Chicken Biryani', + category: 'Food · HotPot', + price: '₹189', + originalPrice: '₹240', + image: 'https://images.unsplash.com/photo-1563379091339-03b21ab4a4f8?w=300&fit=crop', + rating: 4.8, + reviewCount: 320, + ), + ProductItem( + name: 'Fresh Veggie Basket', + category: 'Grocery · BigBasket', + price: '₹299', + originalPrice: '₹420', + image: 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=300&fit=crop', + rating: 4.5, + reviewCount: 180, + ), + ProductItem( + name: 'Margherita Pizza', + category: 'Food · Dominos', + price: '₹149', + originalPrice: '₹199', + image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=300&fit=crop', + rating: 4.6, + reviewCount: 512, + ), + ProductItem( + name: 'Birthday Cake', + category: 'Bakery · CakeZone', + price: '₹549', + originalPrice: '₹699', + image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&fit=crop', + rating: 4.9, + reviewCount: 94, + ), + ProductItem( + name: 'Mango Smoothie', + category: 'Drinks · Juice Bar', + price: '₹89', + originalPrice: '₹120', + image: 'https://images.unsplash.com/photo-1623065422902-30a2d299bbe4?w=300&fit=crop', + rating: 4.4, + reviewCount: 67, + ), + ProductItem( + name: 'Paneer Butter Masala', + category: 'Food · Behrouz', + price: '₹220', + originalPrice: '₹280', + image: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc7?w=300&fit=crop', + rating: 4.7, + reviewCount: 210, + ), + ]; + bool status = true; bool _showBackToTop = false; RxBool showMiniCart = false.obs; @@ -84,177 +172,6 @@ class _DashboardPageState extends State { Icons.shopping_cart_rounded, ]; - void _openCategoryBottomSheet( - BuildContext context, - List subCategories, - item, - ) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) { - return SafeArea( - top: false, - child: ClipRRect( - borderRadius: - const BorderRadius.vertical(top: Radius.circular(20)), - child: Container( - height: MediaQuery.of(context).size.height * 0.85, - color: Colors.white, - child: Column( - - children: [ - const SizedBox(height: 8), - - // drag handle (smaller) - Container( - width: 36, - height: 4, - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(10), - ), - ), - - const SizedBox(height: 10), - - // Title (not oversized) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 14), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - "Category", - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - - - - const SizedBox(height: 10), - - Expanded( - child: subCategories.isEmpty - ? const Center( - child: Text("No products available"), - ) - : GridView.builder( - padding: - const EdgeInsets.fromLTRB(12, 8, 12, 16), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.78, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemCount: subCategories.length, - itemBuilder: (context, index) { - final product = subCategories[index]; - - return InkWell( - borderRadius: - BorderRadius.circular(14), - onTap: () { - Navigator.pop(context); - Get.to( - () => SubCategoryProductsScreen( - tenantId: item.tenantid!, - locationId: item.locationid!, - categoryId: item.categoryid!, - tenantName: item.tenantname!, - locationname: - item.locationname!, - tenantLocation: item.suburb!, - tenantImage: - item.tenantimage!, - tenantloc: - item.locationid!, - subCategoryName: - product.subcatname - .toString(), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(14), - border: Border.all( - color: Colors.black12, - width: 0.25, - ), - boxShadow: [ - BoxShadow( - color: Colors.black - .withOpacity(0.04), - blurRadius: 5, - ), - ], - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(height: 10), - ClipOval( - child: Image.network( - product.image ?? '', - width: 100, - height: 100, - fit: BoxFit.cover, - errorBuilder: - (_, __, ___) => - const Icon( - Icons - .image_not_supported), - ), - ), - Padding( - padding: - const EdgeInsets.symmetric( - horizontal: 6), - child: ReusableTextWidget( - text: - product.subcatname ?? - '', - color: Colors.black - .withOpacity(0.7), - fontFamily: - FontConstants - .fontFamily, - fontSize: 11.5, - fontWeight: - FontWeight.w600, - textAlign: - TextAlign.center, - maxLines: 1, - overflow: TextOverflow - .ellipsis, - ), - ), - const SizedBox(height: 10), - ], - ), - ), - ); - }, - ), - ), - ], - ), - ), - ), - ); - }, - ); - } @override @@ -659,7 +576,7 @@ class _DashboardPageState extends State { borderRadius: BorderRadius.circular(14), onTap: () => Get.to(() => const SearchScreen()), child: Container( - height: 48, + height: 55, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.white, @@ -710,6 +627,11 @@ class _DashboardPageState extends State { ], ), ), + + IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.mic, color: Colors.black87, size: 22)), + + + IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.camera_alt_outlined, color: Colors.black87, size: 22)), ], ), ), @@ -811,12 +733,226 @@ class _DashboardPageState extends State { ); }), + const SliverToBoxAdapter( + child: SizedBox(height: 12), + ), + + + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recommended for You", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.7), + ), + ), + TextButton( + onPressed: () {}, + child: const Text("See all"), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 110, + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16), + scrollDirection: Axis.horizontal, + itemCount: recommendations.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final item = recommendations[index]; + + return Container( + width: 160, + decoration: BoxDecoration( + color: item.bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Stack( + children: [ + + // Title + Subtitle + Positioned( + top: 14, + left: 14, + right: 14, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Colors.black.withOpacity(0.7), + height: 1.25, + ), + ), + const SizedBox(height: 6), + Text( + item.subtitle, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w600, + color: item.accentColor, + ), + ), + ], + ), + ), + + // Network image bottom-right + Positioned( + bottom: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(20), + ), + child: CachedNetworkImage( + imageUrl: item.image, + width: 80, + height: 80, + fit: BoxFit.contain, + placeholder: (context, url) => const SizedBox( + width: 80, + height: 80, + ), + errorWidget: (context, url, error) => SizedBox( + width: 80, + height: 80, + child: Icon( + Icons.image_not_supported_rounded, + color: item.accentColor.withOpacity(0.3), + size: 28, + ), + ), + ), + ), + ), + + // Arrow button bottom-right corner + Positioned( + bottom: 10, + right: 10, + child: Container( + width: 26, + height: 26, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.chevron_right_rounded, + size: 16, + color: item.accentColor, + ), + ), + ), + + ], + ), + ); + }, + ), + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 12), + ), + + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Fresh picks for you", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.7), + ), + ), + TextButton( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context,) => const ProductsList())); + }, + child: const Text("See all"), + ), + ], + ), + ), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 12), + ), + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + mainAxisExtent: 270, + ), + delegate: SliverChildBuilderDelegate( + (context, index) => ProductCard(item: products[index]), + childCount: products.length, + ), + ), + ), + + + const SliverToBoxAdapter( + child: SizedBox(height: 12), + ), + + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Near by Stores", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.7), + ), + ), + TextButton( + onPressed: () {}, + child: const Text("See all"), + ), + ], + ), + ), + ), @@ -898,7 +1034,7 @@ class _DashboardPageState extends State { (context, index) { final item = tenantController.tenants[index]; return _ZoomOnTap( - onTap: () { + onTap: () async { Get.to(() => ProductsScreen( tenantId: item.tenantid!, locationId: item.locationid!, @@ -910,6 +1046,16 @@ class _DashboardPageState extends State { tenantloc:item.locationid!, subCategoryName: "", )); + + await AnalyticsService.logEvent( + 'store_viewed', + parameters: { + 'store_id': item.tenantid!, + 'store_name': item.tenantname!, + 'locatiom' : item.locationname!, + }, + ); + }, child: Container( margin: const EdgeInsets.only(bottom: 12, top: 12), @@ -1063,7 +1209,7 @@ class _DashboardPageState extends State { maxLines: 1, ), IconButton( - onPressed: () { + onPressed: () async { // _openCategoryBottomSheet( // context, // item.subcategories ?? [], @@ -1082,6 +1228,15 @@ class _DashboardPageState extends State { subCategoryName: "", )); + await AnalyticsService.logEvent( + 'store_viewed', + parameters: { + 'store_id': item.tenantid!, + 'store_name': item.tenantname!, + 'locatiom' : item.locationname!, + }, + ); + }, icon: Icon(Icons.arrow_circle_right_outlined, color: Colors.black.withOpacity(0.6), @@ -1113,7 +1268,7 @@ class _DashboardPageState extends State { final product = item.subcategories![index]; return InkWell( borderRadius: BorderRadius.circular(16), - onTap: () { + onTap: () async { // Get.to(() => SubCategoryProductsScreen( // tenantId: item.tenantid!, @@ -1139,6 +1294,15 @@ class _DashboardPageState extends State { subCategoryName: "", )); + await AnalyticsService.logEvent( + 'store_viewed', + parameters: { + 'store_id': item.tenantid!, + 'store_name': item.tenantname!, + 'locatiom' : item.locationname!, + }, + ); + }, child: Container( margin: @@ -1247,11 +1411,6 @@ class _DashboardPageState extends State { ), ), ), - - - - - Obx(() { if (cartController.cartItems.isEmpty) return const SizedBox(); final tenant = cartController.currentTenant.value; @@ -1374,45 +1533,12 @@ class _DashboardPageState extends State { ), ); }), - - - - - - - - - - - - ],), ), ); } - Widget _chip(String emoji, String label) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: Color(0xFFF8F4FF), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: Color(0xFFE0D4FF), width: 1), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(emoji, style: TextStyle(fontSize: 13)), - SizedBox(width: 5), - Text( - label, - style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF333333)), - ), - ], - ), - ); - } @@ -1628,23 +1754,6 @@ class _DashboardPageState extends State { ); } - Widget _buildBannerShimmer() { - return SliverToBoxAdapter( - child: Shimmer.fromColors( - baseColor: Colors.grey.shade300, - highlightColor: Colors.grey.shade100, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - height: 180, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(14), - ), - ), - ), - ); - } Widget _buildListShimmer(BuildContext context) { return SliverList( @@ -1834,4 +1943,5 @@ class _ZoomOnTapState extends State<_ZoomOnTap> { ), ); } -} \ No newline at end of file +} + diff --git a/lib/view/dashboard_view/searchScreen.dart b/lib/view/dashboard_view/searchScreen.dart index 532d521..d38cc0a 100644 --- a/lib/view/dashboard_view/searchScreen.dart +++ b/lib/view/dashboard_view/searchScreen.dart @@ -1,610 +1,495 @@ -import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:http/http.dart' as http; +import 'package:lottie/lottie.dart'; import 'package:nearledaily/constants/color_constants.dart'; +import 'package:speech_to_text/speech_to_text.dart'; +import '../../controllers/search_controller/search_controller.dart'; +import '../../widgets/search_widgets.dart'; -import '../../constants/font_constants.dart'; -import '../../controllers/tenant_controller /tenant_list.dart'; -import '../../widgets/text_widget.dart'; -import '../product/tenant_products.dart'; - -// ─── Search result modules ───────────────────────────────────────────────────── - -class _SearchResult { - final String tenantName; - final String productName; - final String subCatName; - - const _SearchResult({ - required this.tenantName, - required this.productName, - required this.subCatName, - }); - - factory _SearchResult.fromJson(Map json) => _SearchResult( - tenantName: json['tenantname'] ?? '', - productName: json['productname'] ?? '', - subCatName: json['subcatname'] ?? '', - ); -} - -// ─── Screen ────────────────────────────────────────────────────────────────── +// ─── Main Screen ────────────────────────────────────────────────────────────── class SearchScreen extends StatefulWidget { - const SearchScreen({Key? key}) : super(key: key); + final bool autoListen; + const SearchScreen({super.key,this.autoListen = false}); @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { + late final GrocerySearchController _ctrl; + final SpeechToText _speech = SpeechToText(); final TextEditingController _searchController = TextEditingController(); - final FocusNode _focusNode = FocusNode(); - final TenantController tenantController = Get.find(); - - late AnimationController _animationController; - late Animation _fadeAnimation; - late Animation _slideAnimation; - - Timer? _debounce; - bool _isSearching = false; - List<_SearchResult> _searchResults = []; - String _lastQuery = ''; - - static const String _searchBaseUrl = - 'https://fiesta.nearle.app/live/api/v1/mob/tenants/searchbykeyword'; - + bool _isListening = true; @override void initState() { super.initState(); + _ctrl = GrocerySearchController(); + _ctrl.init(this); + _ctrl.addListener(() => setState(() {})); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 400), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.autoListen) { + startListening(); + } + }); - _fadeAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - ); + } + Future startListening() async { + bool available = await _speech.initialize(); - _slideAnimation = Tween( - begin: const Offset(0, 0.05), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - )); - - _animationController.forward(); - _focusNode.addListener(() => setState(() {})); - _searchController.addListener(_onSearchChanged); + if (available) { + _speech.listen( + onResult: (result) { + _searchController.text = result.recognizedWords; + }, + ); + } } @override void dispose() { - _debounce?.cancel(); - _animationController.dispose(); - _searchController.removeListener(_onSearchChanged); - _searchController.dispose(); - _focusNode.dispose(); + _ctrl.disposeAll(); super.dispose(); } - void _onSearchChanged() { - final query = _searchController.text.trim(); - setState(() {}); - - if (query == _lastQuery) return; - _lastQuery = query; - _debounce?.cancel(); - - if (query.isEmpty) { - setState(() { - _searchResults = []; - _isSearching = false; - }); - return; - } - - _debounce = Timer(const Duration(milliseconds: 400), () { - _fetchSearchResults(query); - }); - } - - Future _fetchSearchResults(String keyword) async { - if (!mounted) return; - setState(() => _isSearching = true); - - try { - final uri = Uri.parse( - '$_searchBaseUrl?keyword=${Uri.encodeComponent(keyword)}'); - final response = - await http.get(uri).timeout(const Duration(seconds: 10)); - - if (!mounted) return; - - if (response.statusCode == 200) { - final data = jsonDecode(response.body) as Map; - if (data['status'] == true && data['details'] is List) { - final results = (data['details'] as List) - .map((e) => _SearchResult.fromJson(e as Map)) - .toList(); - setState(() { - _searchResults = results; - _isSearching = false; - }); - return; - } - } - setState(() { - _searchResults = []; - _isSearching = false; - }); - } catch (_) { - if (!mounted) return; - setState(() { - _searchResults = []; - _isSearching = false; - }); - } - } - - List get _filteredTenants { - final query = _searchController.text.trim(); - if (query.isEmpty) return tenantController.searchtenants; - - final matchedNames = - _searchResults.map((r) => r.tenantName.toLowerCase()).toSet(); - - return tenantController.searchtenants - .where( - (t) => matchedNames.contains((t.tenantname ?? '').toLowerCase())) - .toList(); - } - - Widget _imagePlaceholder() { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.grey.shade100, Colors.grey.shade400], - ), - ), - child: Center( - child: Icon(Icons.store_outlined, - size: 48, color: Colors.grey.shade400), - ), - ); - } - - void _navigateTo(dynamic item) { - HapticFeedback.lightImpact(); - Get.to( - () => ProductsScreen( - tenantId: item.tenantid!, - locationId: item.locationid!, - categoryId: item.categoryid!, - tenantName: item.tenantname!, - locationname: item.locationname!, - tenantLocation: item.suburb!, - tenantImage: item.tenantimage!, - tenantloc: item.locationid!, - subCategoryName: "", - ), - transition: Transition.cupertino, - duration: const Duration(milliseconds: 300), - ); - } + // ─── Build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFF6F6F6), - body: Column( + backgroundColor: const Color(0xFFF4F6F3), + body: Stack( children: [ - // ── Search bar ──────────────────────────────────────────────── - SafeArea( - bottom: false, - child: FadeTransition( - opacity: _fadeAnimation, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: Hero( - tag: 'search_bar', - child: Material( - color: Colors.transparent, - child: Container( - height: 52, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: _focusNode.hasFocus - ? ColorConstants.primaryColor - : Colors.grey.shade300, - width: _focusNode.hasFocus ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: _focusNode.hasFocus - ? ColorConstants.primaryColor.withOpacity(0.15) - : Colors.black.withOpacity(0.06), - blurRadius: _focusNode.hasFocus ? 12 : 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), - ), - const SizedBox(width: 6), - Expanded( - child: TextField( - controller: _searchController, - focusNode: _focusNode, - autofocus: true, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - decoration: InputDecoration( - hintText: 'Search stores or products...', - hintStyle: TextStyle( - color: Colors.grey.shade400, - fontWeight: FontWeight.normal, - ), - border: InputBorder.none, - ), - ), - ), - if (_isSearching) - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: ColorConstants.primaryColor, - ), - ) - else if (_searchController.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear, size: 20), - onPressed: () { - _searchController.clear(); - setState(() { - _searchResults = []; - _lastQuery = ''; - }); - }, - ), - ], - ), - ), - ), + + // Center( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // SizedBox( + // height: 220, + // width: 220, + // child: Lottie.asset( + // 'assets/lotties/ai.json', + // repeat: true, + // ), + // ), + // + // const SizedBox(height: 20), + // + // Text( + // "Listening...", + // style: TextStyle( + // fontSize: 24, + // fontWeight: FontWeight.w600, + // ), + // ), + // + // const SizedBox(height: 8), + // + // Text( + // "Speak what you're looking for", + // style: TextStyle( + // color: Colors.grey, + // fontSize: 16, + // ), + // ), + // ], + // ), + // ), + CustomScrollView( + slivers: [ + _buildSliverHeader(), + SliverToBoxAdapter(child: _buildSearchBar()), + SliverToBoxAdapter( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: _ctrl.hasSearched + ? _buildResults() + : _buildSuggestions(), ), ), - ), - ), - - // ── List ────────────────────────────────────────────────────── - Expanded( - child: Obx(() { - if (tenantController.isLoading.value) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: ColorConstants.primaryColor), - const SizedBox(height: 16), - Text( - 'Loading stores...', - style: TextStyle( - color: Colors.grey.shade600, fontSize: 14), - ), - ], - ), - ); - } - - final displayTenants = _filteredTenants; - final hasQuery = _searchController.text.trim().isNotEmpty; - - if (displayTenants.isEmpty) { - return FadeTransition( - opacity: _fadeAnimation, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off_rounded, - size: 80, color: Colors.grey.shade300), - const SizedBox(height: 16), - Text( - hasQuery - ? 'No matching stores found' - : 'No stores found', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 8), - Text( - 'Try searching with different keywords', - style: TextStyle( - fontSize: 14, color: Colors.grey.shade500), - ), - ], - ), - ), - ); - } - - return ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 30), - physics: const BouncingScrollPhysics(), - itemCount: displayTenants.length, - itemBuilder: (context, index) { - final item = displayTenants[index]; - - return FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: _StoreListItem( - item: item, - index: index, - imagePlaceholder: _imagePlaceholder(), - onTap: () => _navigateTo(item), - ), - ), - ); - }, - ); - }), + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], ), + _buildCartBar(), ], ), ); } -} -// ─── List item: banner image on top (outside card), info card below ────────── + // ─── Header ───────────────────────────────────────────────────────────────── -class _StoreListItem extends StatelessWidget { - final dynamic item; - final int index; - final Widget imagePlaceholder; - final VoidCallback onTap; - - const _StoreListItem({ - required this.item, - required this.index, - required this.imagePlaceholder, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final hasBanner = item.tenantbanner != null && - (item.tenantbanner as String).isNotEmpty; - - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── Banner — tappable, rounded top corners, NO card background ─ - GestureDetector( - onTap: onTap, - child: Hero( - tag: 'store_${item.tenantid}_$index', - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - child: Container( - height: 180, - width: double.infinity, - color: Colors.grey.shade200, - child: hasBanner - ? Image.network( - item.tenantbanner as String, - fit: BoxFit.cover, - loadingBuilder: (ctx, child, progress) => - progress == null ? child : _ShimmerLoading(), - errorBuilder: (ctx, _, __) => imagePlaceholder, - ) - : imagePlaceholder, - ), - ), - ), - ), - - // ── Info card — flush below the image ─────────────────────── - _StoreInfoCard(item: item, onTap: onTap), - ], + Widget _buildSliverHeader() { + return SliverAppBar( + pinned: true, + backgroundColor: Color(0xFFF4F6F3), + elevation: 0, + flexibleSpace: FlexibleSpaceBar( + background: Container(color: Color(0xFFF4F6F3)), + ), + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back, color: Colors.black), + ), + title: const Text( + 'Search', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), ), ); } -} -// ─── Info card: name + location only ───────────────────────────────────────── + // ─── Search Bar ───────────────────────────────────────────────────────────── -class _StoreInfoCard extends StatefulWidget { - final dynamic item; - final VoidCallback onTap; - - const _StoreInfoCard({required this.item, required this.onTap}); - - @override - State<_StoreInfoCard> createState() => _StoreInfoCardState(); -} - -class _StoreInfoCardState extends State<_StoreInfoCard> - with SingleTickerProviderStateMixin { - late AnimationController _scaleController; - late Animation _scaleAnimation; - bool _isPressed = false; - - @override - void initState() { - super.initState(); - _scaleController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 150), - ); - _scaleAnimation = Tween(begin: 1.0, end: 0.98).animate( - CurvedAnimation(parent: _scaleController, curve: Curves.easeInOut), - ); - } - - @override - void dispose() { - _scaleController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTapDown: (_) { - setState(() => _isPressed = true); - _scaleController.forward(); - }, - onTapUp: (_) { - setState(() => _isPressed = false); - _scaleController.reverse(); - widget.onTap(); - }, - onTapCancel: () { - setState(() => _isPressed = false); - _scaleController.reverse(); - }, - child: ScaleTransition( - scale: _scaleAnimation, - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + Widget _buildSearchBar() { + return Transform.translate( + offset: const Offset(0, -20), + child: Padding( + padding: const EdgeInsets.only(top: 30), + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), decoration: BoxDecoration( color: Colors.white, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8E0), width: 1), boxShadow: [ BoxShadow( - color: _isPressed - ? Colors.black.withOpacity(0.04) - : Colors.black.withOpacity(0.08), - blurRadius: _isPressed ? 3 : 6, - offset: Offset(0, _isPressed ? 1 : 3), + color: Colors.black.withOpacity(0.08), + blurRadius: 11, + offset: const Offset(0, 2), ), ], ), child: Row( children: [ + const Padding( + padding: EdgeInsets.only(left: 14, right: 8), + child: Icon(Icons.search_rounded, + color: Color(0xFF6B7A72), size: 22), + ), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Store name - ReusableTextWidget( - text: widget.item.tenantname ?? '', - fontFamily: FontConstants.fontFamily, - fontSize: 16, - fontWeight: FontWeight.bold, + child: TextField( + controller: _ctrl.textController, + focusNode: _ctrl.focusNode, + onSubmitted: _ctrl.search, + textInputAction: TextInputAction.search, + style: const TextStyle( + fontSize: 15, color: Color(0xFF1A1F1C)), + decoration: const InputDecoration( + hintText: 'Ask anything — biryani, cold remedy...', + hintStyle: + TextStyle(color: Color(0xFF6B7A72), fontSize: 14), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 14), + ), + ), + ), + if (_ctrl.textController.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear_rounded, + color: Color(0xFF6B7A72), size: 18), + onPressed: _ctrl.clearSearch, + ), + Container( + margin: const EdgeInsets.all(6), + child: _ctrl.loading + ? SizedBox( + width: 50, + height: 50, + child: Lottie.asset( + 'assets/lotties/ai_loader.json', + repeat: true, + ), + ) + : ElevatedButton.icon( + onPressed: () => _ctrl.search(_ctrl.textController.text), + icon: const Icon( + Icons.auto_awesome_rounded, + size: 14, + ), + label: const Text('Find'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF662582), + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, ), - const SizedBox(height: 4), - // Location - Row( - children: [ - Icon(Icons.location_on_outlined, - size: 13, color: Colors.grey.shade500), - const SizedBox(width: 3), - Expanded( - child: ReusableTextWidget( - text: widget.item.locationname ?? '', - fontSize: 12, - color: Colors.black54, - ), - ), - ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(11), + ), + ), + ), + ) + ], + ), + ), + ), + ); + } + + // ─── Suggestions ──────────────────────────────────────────────────────────── + + Widget _buildSuggestions() { + return Padding( + key: const ValueKey('suggestions'), + padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SectionLabel('Quick picks'), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: _ctrl.quickChips + .map((c) => SearchChip( + label: c['label']!, + onTap: () => _ctrl.fillSearch(c['query']!), + )) + .toList(), + ), + const SizedBox(height: 18), + const SectionLabel('Or tell us naturally'), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: _ctrl.naturalChips + .map((c) => SearchChip( + label: c['label']!, + onTap: () => _ctrl.fillSearch(c['query']!), + muted: true, + )) + .toList(), + ), + ], + ), + ); + } + + // ─── Results ──────────────────────────────────────────────────────────────── + + Widget _buildResults() { + return Padding( + key: const ValueKey('results'), + padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Query label + + const SizedBox(height: 10), + + // Loading skeletons + if (_ctrl.loading) + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: 4, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.88, + ), + itemBuilder: (_, __) => const SkeletonCard(), + ), + + // Results header + add-all + if (!_ctrl.loading && _ctrl.items.isNotEmpty) ...[ + Row( + children: [ + const Text( + 'Products to buy', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1F1C)), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFEDF2F0), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${_ctrl.items.length} items', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF6B7A72)), + ), + ), + ], + ), + const SizedBox(height: 10), + GestureDetector( + onTap: _ctrl.allInCart ? null : _ctrl.addAllToCart, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _ctrl.allInCart + ? const Color(0xFFE8F5EE) + : const Color(0xFF1A6B3C), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _ctrl.allInCart + ? Icons.check_circle_rounded + : Icons.add_shopping_cart_rounded, + color: _ctrl.allInCart + ? const Color(0xFF1A6B3C) + : Colors.white, + size: 18, + ), + const SizedBox(width: 6), + Text( + _ctrl.allInCart + ? 'All items in cart' + : 'Add all to cart', + style: TextStyle( + color: _ctrl.allInCart + ? const Color(0xFF1A6B3C) + : Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), ], ), ), - Icon(Icons.arrow_forward_ios_rounded, - size: 14, color: Colors.grey.shade400), + ), + const SizedBox(height: 12), + GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _ctrl.items.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.88, + ), + itemBuilder: (context, index) => GroceryCard( + item: _ctrl.items[index], + onToggle: () => _ctrl.toggleCart(_ctrl.items[index]), + ), + ), + ], + + // Empty state + if (!_ctrl.loading && _ctrl.items.isEmpty && _ctrl.hasSearched) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 40), + child: Column( + children: [ + const Icon(Icons.sentiment_dissatisfied_rounded, + size: 40, color: Color(0xFFC0CFC5)), + const SizedBox(height: 12), + Text( + 'No products found.\nTry a different search.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + height: 1.6), + ), + ], + ), + ), + ), + ], + ), + ); + } + + // ─── Cart Bar ──────────────────────────────────────────────────────────────── + + Widget _buildCartBar() { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: SlideTransition( + position: _ctrl.cartBarSlide, + child: Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + decoration: const BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Color(0xFFE2E8E0), width: 1.5)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF1A6B3C), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_ctrl.cartItems.length}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 13), + ), + ), + const SizedBox(width: 10), + const Text( + 'items in cart', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1F1C)), + ), + const Spacer(), + ElevatedButton.icon( + onPressed: _showCartSheet, + icon: const Icon(Icons.arrow_forward_rounded, size: 16), + label: const Text('Checkout'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A6B3C), + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 11), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + textStyle: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w600), + ), + ), ], ), ), ), ); } -} -// ─── Shimmer loading ────────────────────────────────────────────────────────── - -class _ShimmerLoading extends StatefulWidget { - @override - State<_ShimmerLoading> createState() => _ShimmerLoadingState(); -} - -class _ShimmerLoadingState extends State<_ShimmerLoading> - with SingleTickerProviderStateMixin { - late AnimationController _ctrl; - - @override - void initState() { - super.initState(); - _ctrl = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - )..repeat(); - } - - @override - void dispose() { - _ctrl.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _ctrl, - builder: (_, __) => Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.grey.shade200, - Colors.grey.shade100, - Colors.grey.shade200, - ], - stops: [ - (_ctrl.value - 0.3).clamp(0.0, 1.0), - _ctrl.value.clamp(0.0, 1.0), - (_ctrl.value + 0.3).clamp(0.0, 1.0), - ], - ), - ), + void _showCartSheet() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (_) => CartSheet( + items: _ctrl.cartItems, + onRemove: (item) { + _ctrl.removeFromCart(item); + if (_ctrl.cartItems.isEmpty) Navigator.pop(context); + }, ), ); } diff --git a/lib/view/dashboard_view/tenant_profile.dart b/lib/view/dashboard_view/tenant_profile.dart index f34c618..1aa7ad3 100644 --- a/lib/view/dashboard_view/tenant_profile.dart +++ b/lib/view/dashboard_view/tenant_profile.dart @@ -81,6 +81,210 @@ class TenantApiService { } } +// ───────────────────────────────────────────── +// SHIMMER WIDGET +// ───────────────────────────────────────────── +class _ShimmerBox extends StatefulWidget { + final double width; + final double height; + final double borderRadius; + + const _ShimmerBox({ + required this.width, + required this.height, + this.borderRadius = 8, + }); + + @override + State<_ShimmerBox> createState() => _ShimmerBoxState(); +} + +class _ShimmerBoxState extends State<_ShimmerBox> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); + _animation = Tween(begin: -1.5, end: 1.5).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (_, __) => Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + gradient: LinearGradient( + begin: Alignment(_animation.value - 1, 0), + end: Alignment(_animation.value, 0), + colors: const [ + Color(0xFFE8E8E8), + Color(0xFFF5F5F5), + Color(0xFFE8E8E8), + ], + ), + ), + ), + ); + } +} + +// Shimmer skeleton that mimics the actual screen layout +class _StoreOverviewShimmer extends StatelessWidget { + const _StoreOverviewShimmer(); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top bar + Row( + children: [ + _ShimmerBox(width: 40, height: 40, borderRadius: 20), + ], + ), + const SizedBox(height: 16), + + // Store card shimmer + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ShimmerBox(width: 200, height: 22, borderRadius: 6), + const SizedBox(height: 10), + _ShimmerBox(width: double.infinity, height: 13, borderRadius: 4), + const SizedBox(height: 6), + _ShimmerBox(width: 160, height: 13, borderRadius: 4), + const SizedBox(height: 14), + Row( + children: [ + _ShimmerBox(width: 40, height: 40, borderRadius: 20), + const SizedBox(width: 12), + _ShimmerBox(width: 40, height: 40, borderRadius: 20), + ], + ), + const Divider(height: 24, thickness: 0.5), + Row( + children: [ + _ShimmerBox(width: 20, height: 20, borderRadius: 4), + const SizedBox(width: 12), + _ShimmerBox(width: 100, height: 13, borderRadius: 4), + ], + ), + const Divider(height: 24, thickness: 0.5), + Row( + children: [ + _ShimmerBox(width: 20, height: 20, borderRadius: 4), + const SizedBox(width: 12), + _ShimmerBox(width: 150, height: 13, borderRadius: 4), + ], + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Bad experience card shimmer + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + _ShimmerBox(width: 36, height: 36, borderRadius: 18), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ShimmerBox(width: 180, height: 13, borderRadius: 4), + const SizedBox(height: 6), + _ShimmerBox(width: 140, height: 11, borderRadius: 4), + ], + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Legal card shimmer + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(4, (i) => Padding( + padding: EdgeInsets.only(bottom: i < 3 ? 16.0 : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ShimmerBox(width: 80, height: 11, borderRadius: 4), + const SizedBox(height: 5), + _ShimmerBox(width: 160, height: 13, borderRadius: 4), + ], + ), + )), + ), + ), + ], + ), + ); + } +} + // ───────────────────────────────────────────── // SCREEN // ───────────────────────────────────────────── @@ -285,9 +489,16 @@ class _StoreOverviewScreenState extends State { child: FutureBuilder( future: _future, builder: (context, snap) { + // ── SHIMMER while loading ────────────── if (snap.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); + return Column( + children: [ + Expanded(child: const _StoreOverviewShimmer()), + _bottomButton(), + ], + ); } + if (snap.hasError) { return Center( child: Column( diff --git a/lib/view/dashboard_view/test.dart b/lib/view/dashboard_view/test.dart new file mode 100644 index 0000000..251c3ca --- /dev/null +++ b/lib/view/dashboard_view/test.dart @@ -0,0 +1,560 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +// ───────────────────────────────────────────── +// CONFIG — change IP to your Mac's local IP +// Run: ipconfig getifaddr en0 +// ───────────────────────────────────────────── +const String kBaseUrl = 'http://192.168.1.XXX:8000'; +const int kCustomerId = 6060; +const double kLatitude = 11.0168; +const double kLongitude = 76.9558; + +// ───────────────────────────────────────────── +// API SERVICE +// ───────────────────────────────────────────── +class AgentService { + static Future> findIngredients(String userInput) async { + final res = await http.post( + Uri.parse('$kBaseUrl/find-ingredients'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'user_input': userInput, + 'customer_id': kCustomerId, + 'latitude': kLatitude, + 'longitude': kLongitude, + }), + ); + return jsonDecode(res.body); + } + + static Future> chat( + String message, + List conversationHistory, + ) async { + final res = await http.post( + Uri.parse('$kBaseUrl/chat'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'message': message, + 'customer_id': kCustomerId, + 'latitude': kLatitude, + 'longitude': kLongitude, + 'conversation_history': conversationHistory, + }), + ); + return jsonDecode(res.body); + } + + static Future> clarify( + String originalInput, + String clarification, + ) async { + final res = await http.post( + Uri.parse('$kBaseUrl/clarify'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'original_input': originalInput, + 'clarification': clarification, + 'customer_id': kCustomerId, + 'latitude': kLatitude, + 'longitude': kLongitude, + }), + ); + return jsonDecode(res.body); + } +} + +// ───────────────────────────────────────────── +// SEARCH SCREEN +// ───────────────────────────────────────────── +class SearchScreen extends StatefulWidget { + const SearchScreen({super.key}); + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + + bool _isLoading = false; + String? _agentMessage; + String? _clarificationQuestion; + String? _lastInput; + + List _ingredients = []; + List _stores = []; + + // Quick suggestion chips + final List _suggestions = [ + '🍓 Strawberry', + '🍗 Biryani', + '🤒 Fever medicine', + '🍳 Cook sambar', + '🥛 Milk', + '😴 Bored snacks', + ]; + + // ── Search via /find-ingredients ────────────────────────── + Future _search(String input) async { + if (input.trim().isEmpty) return; + FocusScope.of(context).unfocus(); + + setState(() { + _isLoading = true; + _agentMessage = null; + _clarificationQuestion = null; + _stores = []; + _ingredients = []; + _lastInput = input; + }); + + try { + final result = await AgentService.findIngredients(input); + + if (result['status'] == 'needs_clarification') { + setState(() { + _clarificationQuestion = result['question']; + _isLoading = false; + }); + return; + } + + setState(() { + _agentMessage = result['agent_message']; + _ingredients = result['ingredients_needed'] ?? []; + _stores = result['stores'] ?? []; + _isLoading = false; + }); + } catch (e) { + setState(() { + _agentMessage = 'Something went wrong. Please try again.'; + _isLoading = false; + }); + } + } + + // ── Answer agent clarification ──────────────────────────── + Future _answerClarification(String answer) async { + setState(() { + _clarificationQuestion = null; + _isLoading = true; + }); + + try { + final result = await AgentService.clarify(_lastInput ?? '', answer); + setState(() { + _agentMessage = result['agent_message']; + _ingredients = result['ingredients_needed'] ?? []; + _stores = result['stores'] ?? []; + _isLoading = false; + }); + } catch (e) { + setState(() { + _agentMessage = 'Something went wrong.'; + _isLoading = false; + }); + } + } + + // ───────────────────────────────────────────────────────── + // BUILD + // ───────────────────────────────────────────────────────── + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + title: const Text( + 'Nearle', + style: TextStyle( + color: Color(0xFF1A1A1A), + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + centerTitle: false, + ), + body: Column( + children: [ + // ── Search Bar ────────────────────────────────── + Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + focusNode: _focusNode, + onSubmitted: _search, + decoration: InputDecoration( + hintText: 'Search or ask anything...', + hintStyle: TextStyle(color: Colors.grey[400]), + prefixIcon: const Icon(Icons.search, color: Color(0xFF6C63FF)), + filled: true, + fillColor: const Color(0xFFF0EFFF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 14), + ), + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () => _search(_searchController.text), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFF6C63FF), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.arrow_forward, color: Colors.white, size: 20), + ), + ), + ], + ), + ), + + // ── Suggestion Chips ──────────────────────────── + if (_stores.isEmpty && !_isLoading && _clarificationQuestion == null) + SizedBox( + height: 48, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _suggestions.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, i) { + return GestureDetector( + onTap: () { + final text = _suggestions[i].replaceAll(RegExp(r'[^\w\s]'), '').trim(); + _searchController.text = text; + _search(text); + }, + child: Chip( + label: Text( + _suggestions[i], + style: const TextStyle(fontSize: 13), + ), + backgroundColor: Colors.white, + side: const BorderSide(color: Color(0xFFE0E0E0)), + ), + ); + }, + ), + ), + + const SizedBox(height: 8), + + // ── Body ──────────────────────────────────────── + Expanded( + child: _isLoading + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Color(0xFF6C63FF)), + SizedBox(height: 16), + Text('Anna is finding the best deals...', style: TextStyle(color: Colors.grey)), + ], + ), + ) + : _clarificationQuestion != null + ? _buildClarification() + : _stores.isEmpty + ? _buildEmpty() + : _buildResults(), + ), + ], + ), + ); + } + + // ── Clarification UI ────────────────────────────────────── + Widget _buildClarification() { + final answerController = TextEditingController(); + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFEDE9FF), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Text('🤔', style: TextStyle(fontSize: 24)), + const SizedBox(width: 12), + Expanded( + child: Text( + _clarificationQuestion!, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + TextField( + controller: answerController, + decoration: InputDecoration( + hintText: 'Type your answer...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + ), + onSubmitted: _answerClarification, + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => _answerClarification(answerController.text), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6C63FF), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Send', style: TextStyle(color: Colors.white, fontSize: 16)), + ), + ), + ], + ), + ); + } + + // ── Empty State ─────────────────────────────────────────── + Widget _buildEmpty() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('🛒', style: TextStyle(fontSize: 60)), + const SizedBox(height: 16), + Text( + _agentMessage ?? 'Search for anything\nfrom nearby stores', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: Colors.grey), + ), + ], + ), + ); + } + + // ── Results ─────────────────────────────────────────────── + Widget _buildResults() { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Agent message + if (_agentMessage != null) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFEDE9FF), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Text('✨', style: TextStyle(fontSize: 20)), + const SizedBox(width: 10), + Expanded(child: Text(_agentMessage!, style: const TextStyle(fontSize: 14))), + ], + ), + ), + + // Store cards + ..._stores.map((storeData) => _StoreCard(storeData: storeData)), + ], + ); + } +} + +// ───────────────────────────────────────────── +// STORE CARD WIDGET +// ───────────────────────────────────────────── +class _StoreCard extends StatelessWidget { + final Map storeData; + const _StoreCard({required this.storeData}); + + @override + Widget build(BuildContext context) { + final store = storeData['store'] as Map; + final products = storeData['matched_products'] as List; + final savings = store['total_savings'] ?? 0; + final total = store['total_discounted_price'] ?? 0; + final hasOffers = store['has_offers'] == true; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Store header + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFF0EFFF), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.store, color: Color(0xFF6C63FF), size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + store['tenantname'] ?? '', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + ), + Text( + store['locationname'] ?? '', + style: const TextStyle(color: Colors.grey, fontSize: 13), + ), + ], + ), + ), + if (hasOffers) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'Save ₹${savings.toStringAsFixed(0)}', + style: const TextStyle(color: Color(0xFF2E7D32), fontWeight: FontWeight.bold, fontSize: 12), + ), + ), + ], + ), + ), + + const Divider(height: 1), + + // Products + ...products.map((m) => _ProductRow(matched: m)), + + const Divider(height: 1), + + // Total + Order button + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Total', style: TextStyle(color: Colors.grey, fontSize: 12)), + Text( + '₹${total.toStringAsFixed(2)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color(0xFF1A1A1A)), + ), + ], + ), + const Spacer(), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6C63FF), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const Text('Order Now', style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────────── +// PRODUCT ROW WIDGET +// ───────────────────────────────────────────── +class _ProductRow extends StatelessWidget { + final Map matched; + const _ProductRow({required this.matched}); + + @override + Widget build(BuildContext context) { + final product = matched['product'] as Map; + final emoji = matched['emoji'] ?? '🛒'; + final name = product['productname'] ?? matched['ingredient'] ?? ''; + final price = product['discounted_price'] ?? 0; + final original = product['original_price'] ?? 0; + final hasOffer = product['has_offer'] == true; + final discount = product['discount_percent'] ?? 0; + final quantity = matched['quantity'] ?? ''; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Text(emoji, style: const TextStyle(fontSize: 24)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), + Text(quantity, style: const TextStyle(color: Colors.grey, fontSize: 12)), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '₹${price.toStringAsFixed(2)}', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF1A1A1A)), + ), + if (hasOffer) + Row( + children: [ + Text( + '₹${original.toStringAsFixed(0)}', + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + decoration: TextDecoration.lineThrough, + ), + ), + const SizedBox(width: 4), + Text( + '$discount% off', + style: const TextStyle(color: Color(0xFF2E7D32), fontSize: 11, fontWeight: FontWeight.bold), + ), + ], + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/view/home_view.dart b/lib/view/home_view.dart index 6f63f40..a9681c0 100644 --- a/lib/view/home_view.dart +++ b/lib/view/home_view.dart @@ -7,6 +7,8 @@ import 'package:lottie/lottie.dart'; import 'package:nearledaily/view/qr_scaner/qr_scaner.dart'; import '../constants/font_constants.dart'; import '../controllers/cart_controller/cart.dart'; +import '../features/Store/presentation/screens/store_view.dart'; +import '../features/dashboard/presentation/screens/dash_view.dart'; import '../widgets/text_widget.dart'; import 'account/account_view.dart'; import 'cart/cart_view.dart'; @@ -22,8 +24,9 @@ const Color _kInactive = Color(0xFFCBA8E4); // ─── Screens ────────────────────────────────────────────────────────────────── final List _screens = [ - DashboardPage(), - const OrdersByStoreScreen(showBackArrow: false), + HomeScreen(), + StoreViewPage(), + // const OrdersByStoreScreen(showBackArrow: false), QrScannerPage(), CartPage(), AccountPage(), @@ -131,18 +134,29 @@ class BottomNavigation extends StatelessWidget { ); } - return Scaffold( - backgroundColor: Colors.white, - extendBody: true, - bottomNavigationBar: Obx( - () => _BottomNavBar( - currentIndex: controller.currentIndex.value, - cartController: cartController, - onTap: (i) => controller.currentIndex.value = i, + return PopScope( + canPop: controller.currentIndex.value == 0, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + + // Not on Home tab -> go back to Home instead of exiting/popping + if (controller.currentIndex.value != 0) { + controller.currentIndex.value = 0; + } + }, + child: Scaffold( + backgroundColor: Colors.white, + extendBody: true, + bottomNavigationBar: Obx( + () => _BottomNavBar( + currentIndex: controller.currentIndex.value, + cartController: cartController, + onTap: (i) => controller.currentIndex.value = i, + ), + ), + body: Obx( + () => _screens[controller.currentIndex.value], ), - ), - body: Obx( - () => _screens[controller.currentIndex.value], ), ); }); @@ -240,8 +254,8 @@ class _GlassPill extends StatelessWidget { onTap: () => onTap(0), ), _NavItem( - icon: Icons.receipt_long_rounded, - label: 'Order', + icon: Icons.storefront, + label: 'Stores', isActive: currentIndex == 1, onTap: () => onTap(1), ), @@ -440,4 +454,4 @@ class _CartNavItem extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/view/orders/order_succes.dart b/lib/view/orders/order_succes.dart index 4479ec2..8963b7b 100644 --- a/lib/view/orders/order_succes.dart +++ b/lib/view/orders/order_succes.dart @@ -5,6 +5,7 @@ import 'package:nearledaily/constants/color_constants.dart'; import '../../../constants/font_constants.dart'; import '../../controllers/cart_controller/cart.dart'; import '../../controllers/dashboard_controller/dashboard_controller.dart'; +import '../../features/orders/presentation/order_tracking.dart'; import '../../widgets/text_widget.dart'; import '../home_view.dart'; @@ -233,7 +234,44 @@ class _OrderSuccessViewState extends State SizedBox(height: size.height * 0.015), + // Secondary CTA — Track Order + SizedBox( + width: double.infinity, + height: 56, + child: OutlinedButton( + onPressed: () { + Get.to(() => DeliveryTrackingPage()); + }, + style: OutlinedButton.styleFrom( + foregroundColor: ColorConstants.primaryColor, + side: BorderSide( + color: ColorConstants.primaryColor, + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.local_shipping_outlined, size: 20), + const SizedBox(width: 8), + ReusableTextWidget( + text: 'Track Order', + color: ColorConstants.primaryColor, + fontFamily: FontConstants.fontFamily, + fontSize: 16, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + + SizedBox(height: size.height * 0.015), ], ), ), diff --git a/lib/view/orders/orders_by_tenant.dart b/lib/view/orders/orders_by_tenant.dart index a209cbd..fc06235 100644 --- a/lib/view/orders/orders_by_tenant.dart +++ b/lib/view/orders/orders_by_tenant.dart @@ -7,6 +7,7 @@ import '../../constants/asset_constants.dart'; import '../../constants/color_constants.dart'; import '../../constants/font_constants.dart'; import '../../controllers/tenant/get_tenant.dart'; // OrderedTenantController +import '../../features/orders/presentation/order_tracking.dart'; import '../../widgets/text_widget.dart'; import 'my_orders.dart'; // OrderDatum @@ -450,149 +451,126 @@ class _OrdersByStoreScreenState extends State height: 5, ), Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - - GestureDetector( - onTap: () async { - final uri = Uri(scheme: 'tel', path: order.pickupcontactno!); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ReusableTextWidget( - text: 'Contact :', - color: ColorConstants.blackColor.withOpacity(0.67), - fontWeight: FontWeight.w600, - fontSize: 13, - fontFamily: FontConstants.fontFamily, + // Track Button + Expanded( + child: Container( + height: 44, + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: const LinearGradient( + colors: [Color(0xFF662582), Color(0xFF7A2E9C)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.phone_rounded, - size: 14, - color: ColorConstants.primaryColor, + boxShadow: [ + BoxShadow( + color: const Color(0xFF662582).withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () { + HapticFeedback.mediumImpact(); + Get.to(() => const DeliveryTrackingPage()); + }, + child: const Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.location_on_rounded, color: Colors.white, size: 18), + SizedBox(width: 6), + Text( + 'Track', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0.2, + ), + ), + ], ), - const SizedBox(width: 5), - ReusableTextWidget( - text: order.pickupcontactno ?? "No Contact", - color: ColorConstants.primaryColor, - fontWeight: FontWeight.w600, - fontSize: 13, - fontFamily: FontConstants.fontFamily, - ), - ], + ), ), - ], + ), ), ), - - - ElevatedButton( - style: ElevatedButton - .styleFrom( - padding: - const EdgeInsets - .symmetric( - horizontal: 12, - vertical: 5), - shape: - RoundedRectangleBorder( - borderRadius: - BorderRadius.circular( - 8), + // View Details Button + Expanded( + child: Container( + height: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: primaryColor, width: 1.4), + color: Colors.white, ), - backgroundColor: - primaryColor, - ), - onPressed: () { - // ✨ Haptic feedback on button press - HapticFeedback.mediumImpact(); - - // ✨ Smooth page transition - Navigator.push( - context, - PageRouteBuilder( - pageBuilder: (context, - animation, - secondaryAnimation) => - OrderDetailsPage( - orderId: order - .orderid ?? - 'Unknown', - storeName: tenantName, - storeLocation: order - .tenantsuburb ?? - 'Unknown', - tax: order - .totaltaxamount ?? - 0, - gstno: order.gstno ?? "", - fee: order - .deliverycharge ?? - 0, - items: order - .orderdetails - ?.map((item) => - { - 'name': - item.productname ?? 'Unknown', - 'quantity': - item.orderqty ?? 0, - 'productSumPrice': - item.productsumprice ?? 0.0, - 'price': - item.price ?? 0.0, - 'discountamount': - item.price ?? 0.0, - 'image': - item.productimage ?? '', - }) - .toList() ?? - [], - ), - transitionsBuilder: - (context, - animation, - secondaryAnimation, - child) { - return FadeTransition( - opacity: animation, - child: - SlideTransition( - position: - Tween( - begin: - const Offset( - 0.05, 0), - end: - Offset.zero, - ).animate( - animation), - child: child, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () { + HapticFeedback.mediumImpact(); + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + OrderDetailsPage( + orderId: order.orderid ?? 'Unknown', + storeName: tenantName, + storeLocation: order.tenantsuburb ?? 'Unknown', + tax: order.totaltaxamount ?? 0, + gstno: order.gstno ?? "", + fee: order.deliverycharge ?? 0, + items: order.orderdetails + ?.map((item) => { + 'name': item.productname ?? 'Unknown', + 'quantity': item.orderqty ?? 0, + 'productSumPrice': item.productsumprice ?? 0.0, + 'price': item.price ?? 0.0, + 'discountamount': item.price ?? 0.0, + 'image': item.productimage ?? '', + }) + .toList() ?? + [], + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.05, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 300), ), ); }, - transitionDuration: - const Duration( - milliseconds: - 300), + child: Center( + child: Text( + 'View Details', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: primaryColor, + letterSpacing: 0.2, + ), + ), + ), ), - ); - }, - child: const Text( - "View Details", - style: TextStyle( - fontSize: 12, - color: Colors.white), + ), ), ), ], diff --git a/lib/view/product/product_details.dart b/lib/view/product/product_details.dart new file mode 100644 index 0000000..ee2b22f --- /dev/null +++ b/lib/view/product/product_details.dart @@ -0,0 +1,886 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:nearledaily/view/product/products_list.dart'; + +// ─── Entry point ─────────────────────────────────────────────────────────────── + +void showProductSheet(BuildContext context, ProductItem item) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withOpacity(0.4), + builder: (_) => _ProductSheet(item: item), + ); +} + +// ─── Sheet widget ────────────────────────────────────────────────────────────── + +class _ProductSheet extends StatefulWidget { + final ProductItem item; + const _ProductSheet({required this.item}); + + @override + State<_ProductSheet> createState() => _ProductSheetState(); +} + +class _ProductSheetState extends State<_ProductSheet> { + final DraggableScrollableController _drag = DraggableScrollableController(); + + static const double _minSize = 0.54; + static const double _maxSize = 1.0; + + int _qty = 1; + bool _isFav = false; + bool _inCart = false; + double _expansion = 0.0; + + static const Color _purple = Color(0xFF662582); + static const Color _purpleLight = Color(0xFFF0EBFF); + static const Color _dark = Color(0xFF111111); + static const Color _surface = Color(0xFFF8F8F8); + static const Color _muted = Color(0xFF888888); + + @override + void initState() { + super.initState(); + _drag.addListener(_onDrag); + } + + void _onDrag() { + if (!_drag.isAttached) return; + final t = ((_drag.size - _minSize) / (_maxSize - _minSize)).clamp(0.0, 1.0); + if (t > 0.88) { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); + } else { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); + } + setState(() => _expansion = t); + } + + @override + void dispose() { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); + _drag.removeListener(_onDrag); + _drag.dispose(); + super.dispose(); + } + + int get _discount { + final orig = double.tryParse( + widget.item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0; + final cur = double.tryParse( + widget.item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0; + return orig > 0 ? ((orig - cur) / orig * 100).round() : 0; + } + + String get _savedAmount { + final orig = double.tryParse( + widget.item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0; + final cur = double.tryParse( + widget.item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0; + final saved = orig - cur; + return saved > 0 ? '₹${saved.toStringAsFixed(0)}' : ''; + } + + void _snapToFull() => _drag.animateTo(_maxSize, + duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic); + + void _snapToPeek() => _drag.animateTo(_minSize, + duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic); + + @override + Widget build(BuildContext context) { + final topPad = MediaQuery.of(context).padding.top; + final botPad = MediaQuery.of(context).padding.bottom; + final e = _expansion; + final cornerRadius = lerpDouble(24.0, 0.0, e)!; + final imageHeight = lerpDouble(300.0, topPad + 340.0, e)!; + final discount = _discount; + final saved = _savedAmount; + + return DraggableScrollableSheet( + controller: _drag, + initialChildSize: _minSize, + minChildSize: _minSize, + maxChildSize: _maxSize, + snap: true, + snapSizes: const [_minSize, _maxSize], + builder: (context, scrollCtrl) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.vertical(top: Radius.circular(cornerRadius)), + ), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: scrollCtrl, + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Hero ── + _buildHero(imageHeight, cornerRadius, topPad, e), + + // ── Drag handle (peek) ── + if (e < 0.2) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFDDD8F0), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + + // ── Body ── + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tag chip + _tagChip(widget.item.tag), + const SizedBox(height: 12), + + // Product name + Text( + widget.item.name, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: _dark, + letterSpacing: -0.4, + height: 1.2, + ), + ), + const SizedBox(height: 4), + + // Category / store + Row( + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Color(0xFF4CAF50), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + widget.item.category, + style: const TextStyle( + fontSize: 12, + color: _muted, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 18), + + // Price + _buildPriceRow(discount, saved), + const SizedBox(height: 6), + if (saved.isNotEmpty) + Text( + 'You save $saved · Free delivery on this order', + style: const TextStyle(fontSize: 12, color: _muted), + ), + const SizedBox(height: 18), + + // Rating + _buildRatingRow(), + + // Swipe hint + if (e < 0.12) ...[ + const SizedBox(height: 20), + _swipeHint(), + ], + + // Expanded content + if (e > 0.12) ...[ + const SizedBox(height: 24), + + // Highlights + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: e > 0.4 ? 1.0 : 0.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('Why you\'ll love it'), + const SizedBox(height: 10), + _buildHighlights(), + const SizedBox(height: 24), + ], + ), + ), + + // About (derived from name + category) + AnimatedOpacity( + duration: const Duration(milliseconds: 350), + opacity: e > 0.52 ? 1.0 : 0.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel('About'), + const SizedBox(height: 10), + Text( + 'A premium quality ${widget.item.name} carefully ' + 'curated for you. Sourced from trusted partners at ' + '${widget.item.category}. Every order is freshness-' + 'guaranteed with no compromise on quality.', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF555555), + height: 1.8, + ), + ), + const SizedBox(height: 24), + ], + ), + ), + + // Reviews + AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: e > 0.62 ? 1.0 : 0.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDivider(), + const SizedBox(height: 20), + _buildReviewsHeader(), + const SizedBox(height: 12), + _buildReviewCard( + initials: 'PR', + name: 'Priya R.', + date: '2 days ago', + stars: 5, + text: + 'Absolutely love it! Super fresh and delivered on time. Will definitely order again.', + ), + const SizedBox(height: 10), + _buildReviewCard( + initials: 'AK', + name: 'Arjun K.', + date: '1 week ago', + stars: 5, + text: + 'Great quality and packaging. Exactly as described. Fast delivery too!', + ), + const SizedBox(height: 24), + ], + ), + ), + + // Quantity + AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: e > 0.70 ? 1.0 : 0.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDivider(), + const SizedBox(height: 20), + _buildQuantityRow(), + const SizedBox(height: 8), + ], + ), + ), + ], + ], + ), + ), + SizedBox(height: botPad + 90), + ], + ), + ), + ), + + // ── CTA bar ── + _buildCtaBar(botPad), + ], + ), + ); + }, + ); + } + + // ── Hero ──────────────────────────────────────────────────────────────────── + + Widget _buildHero( + double imageHeight, double cornerRadius, double topPad, double e) { + final item = widget.item; + final discount = _discount; + + return Stack( + children: [ + // Product image + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(cornerRadius), + topRight: Radius.circular(cornerRadius), + ), + child: Container( + height: imageHeight, + width: double.infinity, + color: item.bgColor, + child: item.imageUrl.isNotEmpty + ? CachedNetworkImage( + imageUrl: item.imageUrl, + fit: BoxFit.cover, + placeholder: (_, __) => const Center( + child: CircularProgressIndicator( + strokeWidth: 2, color: _purple), + ), + errorWidget: (_, __, ___) => Image.network( + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQfoIZCZOkJq4Yg00gVdyXr49xxOJsFjSjABE_SAzOiDQ&s=10', + fit: BoxFit.cover, + ), + ) + : const SizedBox(), + ), + ), + + // Back / close button (full screen) + // Positioned( + // top: lerpDouble(-60, topPad + 12, e)!, + // left: 14, + // child: AnimatedOpacity( + // opacity: e > 0.7 ? 1.0 : 0.0, + // duration: const Duration(milliseconds: 150), + // child: _circleButton( + // icon: Icons.arrow_back_ios_new_rounded, + // onTap: () { + // HapticFeedback.lightImpact(); + // if (_drag.isAttached && _drag.size > _minSize + 0.05) { + // _snapToPeek(); + // } else { + // Navigator.pop(context); + // } + // }, + // ), + // ), + // ), + + // Share + search (full screen) + + + // Discount badge + if (discount > 0) + Positioned( + bottom: 48, + left: 16, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: _dark, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$discount% OFF', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ), + ), + + + ], + ); + } + + // ── Tag chip ──────────────────────────────────────────────────────────────── + + Widget _tagChip(String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: _purpleLight, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: _purple, + ), + ), + ); + } + + // ── Price ─────────────────────────────────────────────────────────────────── + + Widget _buildPriceRow(int discount, String saved) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.item.price, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: _dark, + letterSpacing: -0.5, + ), + ), + const SizedBox(width: 10), + // Text( + // widget.item.originalPrice, + // style: const TextStyle( + // fontSize: 14, + // color: Color(0xFFBBBBBB), + // decoration: TextDecoration.lineThrough, + // decorationColor: Color(0xFFBBBBBB), + // ), + // ), + if (discount > 0) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _dark, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + '$discount% OFF', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ), + ], + ], + ); + } + + // ── Rating ────────────────────────────────────────────────────────────────── + + Widget _buildRatingRow() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Color(0xFFF0F0F0)), + bottom: BorderSide(color: Color(0xFFF0F0F0)), + ), + ), + child: Row( + children: [ + Row( + children: List.generate( + 5, + (i) => Icon( + i < widget.item.rating.floor() + ? Icons.star_rounded + : Icons.star_border_rounded, + size: 16, + color: Colors.yellow.shade700, + ), + ), + ), + const SizedBox(width: 8), + Text( + widget.item.rating.toStringAsFixed(1), + style: const TextStyle( + fontSize: 13, fontWeight: FontWeight.w700, color: _dark), + ), + const SizedBox(width: 6), + // reviewCount is int — convert directly + Text( + '· ${widget.item.reviewCount} ratings', + style: const TextStyle(fontSize: 12, color: _muted), + ), + const Spacer(), + + ], + ), + ); + } + + // ── Highlights ────────────────────────────────────────────────────────────── + + Widget _buildHighlights() { + final tiles = [ + _HighlightData( + icon: Icons.bolt_rounded, + label: 'Fast\nDelivery', + sub: '20–45 min', + iconColor: _purple, + ), + _HighlightData( + icon: Icons.verified_rounded, + label: 'Quality\nVerified', + sub: 'Trusted partners', + iconColor: const Color(0xFF1B5E20), + ), + _HighlightData( + icon: Icons.refresh_rounded, + label: 'Easy\nReturns', + sub: 'Hassle-free', + iconColor: const Color(0xFFB45309), + ), + _HighlightData( + icon: Icons.lock_outline_rounded, + label: 'Secure\nPay', + sub: '100% safe', + iconColor: const Color(0xFF1565C0), + ), + ]; + + return Row( + children: List.generate(tiles.length, (index) { + final t = tiles[index]; + return Expanded( + child: Container( + margin: EdgeInsets.only(right: index < tiles.length - 1 ? 8 : 0), + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 6), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + children: [ + Icon(t.icon, color: t.iconColor, size: 20), + const SizedBox(height: 6), + Text( + t.label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: t.iconColor, + height: 1.3, + ), + ), + const SizedBox(height: 3), + Text( + t.sub, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 9, color: _muted, height: 1.3), + ), + ], + ), + ), + ); + }), + ); + } + + // ── Reviews ───────────────────────────────────────────────────────────────── + + Widget _buildReviewsHeader() { + return Row( + children: [ + const Text( + 'Customer reviews', + style: TextStyle( + fontSize: 13, fontWeight: FontWeight.w700, color: _dark), + ), + const Spacer(), + // reviewCount is int + + ], + ); + } + + Widget _buildReviewCard({ + required String initials, + required String name, + required String date, + required int stars, + required String text, + }) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 15, + backgroundColor: _purpleLight, + child: Text( + initials, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: _purple), + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _dark)), + Row( + children: List.generate( + stars, + (_) => const Icon(Icons.star_rounded, + size: 11, color: _dark), + ), + ), + ], + ), + const Spacer(), + Text(date, + style: const TextStyle(fontSize: 11, color: _muted)), + ], + ), + const SizedBox(height: 8), + Text( + text, + style: const TextStyle( + fontSize: 12, color: Color(0xFF666666), height: 1.6), + ), + ], + ), + ); + } + + // ── Quantity ───────────────────────────────────────────────────────────────── + + Widget _buildQuantityRow() { + return Row( + children: [ + _sectionLabel('Quantity'), + const SizedBox(width: 16), + _qtyButton( + icon: Icons.remove_rounded, + onTap: () { + if (_qty > 1) { + HapticFeedback.lightImpact(); + setState(() => _qty--); + } + }, + filled: false, + ), + const SizedBox(width: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + transitionBuilder: (child, anim) => + ScaleTransition(scale: anim, child: child), + child: Text( + '$_qty', + key: ValueKey(_qty), + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w700, color: _dark), + ), + ), + const SizedBox(width: 8), + _qtyButton( + icon: Icons.add_rounded, + onTap: () { + HapticFeedback.lightImpact(); + setState(() => _qty++); + }, + filled: true, + ), + ], + ); + } + + // ── CTA bar ────────────────────────────────────────────────────────────────── + + Widget _buildCtaBar(double botPad) { + return Container( + padding: EdgeInsets.fromLTRB(20, 14, 20, botPad + 16), + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Color(0xFFF0F0F0))), + ), + child: Row( + children: [ + // Wishlist + GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + setState(() => _isFav = !_isFav); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 52, + height: 52, + decoration: BoxDecoration( + color: _isFav ? const Color(0xFFFFF5F5) : Colors.white, + border: Border.all( + color: _isFav + ? const Color(0xFFFFD0D0) + : const Color(0xFFE8E8E8), + ), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + _isFav + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + color: _isFav ? const Color(0xFFE24B4A) : _dark, + size: 22, + ), + ), + ), + const SizedBox(width: 10), + + // Add to bag + Expanded( + child: GestureDetector( + onTap: () { + HapticFeedback.mediumImpact(); + setState(() => _inCart = true); + _snapToFull(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + height: 52, + decoration: BoxDecoration( + color: _inCart ? const Color(0xFF2E2E2E) : _dark, + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: Text( + _inCart ? 'Added ✓' : 'Add to cart', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: -0.2, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 10), + + + + ], + ), + ); + } + + // ── Swipe hint ─────────────────────────────────────────────────────────────── + + Widget _swipeHint() { + return GestureDetector( + onTap: _snapToFull, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: _purple.withOpacity(0.2)), + borderRadius: BorderRadius.circular(14), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Swipe up for full details', + style: TextStyle( + fontSize: 13, fontWeight: FontWeight.w600, color: _purple), + ), + SizedBox(width: 6), + Icon(Icons.keyboard_arrow_up_rounded, color: _purple, size: 18), + ], + ), + ), + ); + } + + // ── Helpers ────────────────────────────────────────────────────────────────── + + Widget _buildDivider() => + Container(height: 1, color: const Color(0xFFF0F0F0)); + + Widget _sectionLabel(String label) => Text( + label.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: _muted, + letterSpacing: 1.0, + ), + ); + + Widget _circleButton({required IconData icon, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.85), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 18, color: _dark), + ), + ); + } + + Widget _qtyButton( + {required IconData icon, + required VoidCallback onTap, + required bool filled}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: filled ? _dark : Colors.white, + border: + Border.all(color: filled ? _dark : const Color(0xFFE8E8E8)), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 18, color: filled ? Colors.white : _dark), + ), + ); + } +} + +// ─── Internal data class ──────────────────────────────────────────────────────── + +class _HighlightData { + final IconData icon; + final String label; + final String sub; + final Color iconColor; + const _HighlightData({ + required this.icon, + required this.label, + required this.sub, + required this.iconColor, + }); +} \ No newline at end of file diff --git a/lib/view/product/product_view.dart b/lib/view/product/product_view.dart index 9f70334..93af362 100644 --- a/lib/view/product/product_view.dart +++ b/lib/view/product/product_view.dart @@ -3,14 +3,11 @@ import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:readmore/readmore.dart'; import '../../constants/color_constants.dart'; -import '../../constants/font_constants.dart'; import '../../controllers/cart_controller/cart.dart'; import '../../controllers/product/variant_controller.dart'; import '../../domain/provider/varient/varient_pro.dart'; import '../../modules/product/product.dart'; -import '../../widgets/text_widget.dart'; class ProductViewPage extends StatefulWidget { final Product product; diff --git a/lib/view/product/products_list.dart b/lib/view/product/products_list.dart new file mode 100644 index 0000000..bc4b6a1 --- /dev/null +++ b/lib/view/product/products_list.dart @@ -0,0 +1,878 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:nearledaily/view/product/product_details.dart'; +import 'package:nearledaily/view/product/refill_home_screen.dart'; +import 'package:wheel_chooser/wheel_chooser.dart'; + +import '../../widgets/fav_button.dart'; + +// ───────────────────────────────────────────── +// MODEL +// ───────────────────────────────────────────── + +class ProductItem { + final String name; + final String category; + final String price; + final String originalPrice; + final String imageUrl; + final Color bgColor; + final double rating; + final int reviewCount; + final String tag; + + ProductItem({ + required this.name, + required this.category, + required this.price, + required this.originalPrice, + required this.imageUrl, + required this.bgColor, + required this.rating, + required this.reviewCount, + required this.tag, + }); +} + +// ───────────────────────────────────────────── +// SAMPLE DATA +// ───────────────────────────────────────────── + +final allProducts = [ + ProductItem( + name: 'Margherita Pizza', + category: 'Dominos · 20 min', + price: '₹149', + originalPrice: '₹199', + imageUrl: + 'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500', + bgColor: const Color(0xFFFFF3E0), + rating: 4.6, + reviewCount: 512, + tag: 'Food', + ), + + ProductItem( + name: 'Fresh Veggie Basket', + category: 'BigBasket · 45 min', + price: '₹299', + originalPrice: '₹420', + imageUrl: + 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=500', + bgColor: const Color(0xFFE8F5E9), + rating: 4.5, + reviewCount: 180, + tag: 'Grocery', + ), + ProductItem( + name: 'Margherita Pizza', + category: 'Dominos · 20 min', + price: '₹149', + originalPrice: '₹199', + imageUrl: + 'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500', + bgColor: const Color(0xFFFFF3E0), + rating: 4.6, + reviewCount: 512, + tag: 'Food', + ), + + ProductItem( + name: 'Chicken Biryani', + category: 'HotPot · 30 min', + price: '₹189', + originalPrice: '₹240', + imageUrl: + 'https://images.unsplash.com/photo-1631515243349-e0cb75fb8d3a?w=500', + bgColor: const Color(0xFFFBE9E7), + rating: 4.8, + reviewCount: 320, + tag: 'Food', + ), + ProductItem( + name: 'Margherita Pizza', + category: 'Dominos · 20 min', + price: '₹149', + originalPrice: '₹199', + imageUrl: + 'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500', + bgColor: const Color(0xFFFFF3E0), + rating: 4.6, + reviewCount: 512, + tag: 'Food', + ), + ProductItem( + name: 'Birthday Cake', + category: 'CakeZone · 60 min', + price: '₹549', + originalPrice: '₹699', + imageUrl: + 'https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=500', + bgColor: const Color(0xFFFCE4EC), + rating: 4.9, + reviewCount: 94, + tag: 'Bakery', + ), + ProductItem( + name: 'Margherita Pizza', + category: 'Dominos · 20 min', + price: '₹149', + originalPrice: '₹199', + imageUrl: + 'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500', + bgColor: const Color(0xFFFFF3E0), + rating: 4.6, + reviewCount: 512, + tag: 'Food', + ), +]; + +// ───────────────────────────────────────────── +// FILTER CHIPS DATA +// ───────────────────────────────────────────── + +const filterTabs = ['All', 'Food', 'Grocery', 'Bakery', 'Drinks']; + +// ───────────────────────────────────────────── +// PRODUCTS PAGE +// ───────────────────────────────────────────── + +class ProductsList extends StatefulWidget { + const ProductsList({super.key}); + + @override + State createState() => _ProductsListState(); +} + +class _ProductsListState extends State { + String _selectedFilter = 'All'; + String _sortBy = 'Popular'; + + List get _filtered => _selectedFilter == 'All' + ? allProducts + : allProducts.where((p) => p.tag == _selectedFilter).toList(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF7F7FA), + body: CustomScrollView( + slivers: [ + + // ── App Bar ── + SliverAppBar( + backgroundColor: Colors.white, + elevation: 0, + pinned: true, + title: const Text( + 'Products', + style: TextStyle( + color: Color(0xFF1A1A1A), + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + centerTitle: false, + leading: const BackButton(color: Color(0xFF1A1A1A)), + actions: [ + IconButton( + icon: const Icon(Icons.search_rounded, + color: Color(0xFF1A1A1A)), + onPressed: () { + + Navigator.push(context, MaterialPageRoute(builder: (context) => const AutoRefillDemo())); + }, + ), + IconButton( + icon: const Icon(Icons.tune_rounded, + color: Color(0xFF1A1A1A)), + onPressed: () => _showSortSheet(context), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container( + height: 1, + color: const Color(0xFFEEEEEE), + ), + ), + ), + + // ── Filter chips ── + SliverPersistentHeader( + pinned: true, + delegate: _FilterHeaderDelegate( + selectedFilter: _selectedFilter, + sortBy: _sortBy, + onFilterChanged: (f) => setState(() => _selectedFilter = f), + onSortTap: () => _showSortSheet(context), + ), + ), + + + + + // ── Product grid ── + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.72, + ), + delegate: SliverChildBuilderDelegate( + (context, index) => _ProductCard(item: _filtered[index]), + childCount: _filtered.length, + ), + ), + ), + + ], + ), + ); + } + + void _showSortSheet(BuildContext context) { + final options = ['Popular', 'Price: Low to High', 'Price: High to Low', 'Rating', 'Discount']; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (_) => Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Sort by', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(height: 12), + ...options.map((opt) => InkWell( + onTap: () { + setState(() => _sortBy = opt); + Navigator.pop(context); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Expanded( + child: Text( + opt, + style: TextStyle( + fontSize: 14, + fontWeight: _sortBy == opt + ? FontWeight.w700 + : FontWeight.w400, + color: _sortBy == opt + ? const Color(0xFF7C3AED) + : const Color(0xFF1A1A1A), + ), + ), + ), + if (_sortBy == opt) + const Icon(Icons.check_circle_rounded, + color: Color(0xFF7C3AED), size: 20), + ], + ), + ), + )), + ], + ), + ), + ); + } +} + +// ───────────────────────────────────────────── +// STICKY FILTER HEADER DELEGATE +// ───────────────────────────────────────────── + +class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate { + final String selectedFilter; + final String sortBy; + final ValueChanged onFilterChanged; + final VoidCallback onSortTap; + + _FilterHeaderDelegate({ + required this.selectedFilter, + required this.sortBy, + required this.onFilterChanged, + required this.onSortTap, + }); + + @override + double get minExtent => 56; + @override + double get maxExtent => 56; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Container( + color: Colors.white, + child: Column( + children: [ + Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + scrollDirection: Axis.horizontal, + itemCount: filterTabs.length + 1, // +1 for sort chip + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) { + // Last item = Sort chip + if (index == filterTabs.length) { + return GestureDetector( + onTap: onSortTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF3F0FF), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color(0xFFD4C5FF), width: 1), + ), + child: Row( + children: [ + const Icon(Icons.swap_vert_rounded, + size: 15, color: Color(0xFF662582)), + const SizedBox(width: 4), + Text( + sortBy == 'Popular' ? 'Sort' : sortBy, + style: const TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w600, + color: Color(0xFF662582), + ), + ), + ], + ), + ), + ); + } + + final tab = filterTabs[index]; + final isSelected = selectedFilter == tab; + + return GestureDetector( + onTap: () => onFilterChanged(tab), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF662582) + : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? const Color(0xFF662582) + : const Color(0xFFE0E0E0), + width: 1, + ), + ), + child: Text( + tab, + style: TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.white + : const Color(0xFF757575), + ), + ), + ), + ); + }, + ), + ), + Container(height: 1, color: const Color(0xFFEEEEEE)), + ], + ), + ); + } + + @override + bool shouldRebuild(_FilterHeaderDelegate old) => + old.selectedFilter != selectedFilter || old.sortBy != sortBy; +} + +// ───────────────────────────────────────────── +// PRODUCT CARD +// ───────────────────────────────────────────── + +class _ProductCard extends StatefulWidget { + final ProductItem item; + const _ProductCard({required this.item}); + + @override + State<_ProductCard> createState() => _ProductCardState(); +} + +class _ProductCardState extends State<_ProductCard> { + bool _isFav = false; + + + @override + Widget build(BuildContext context) { + final item = widget.item; + int selectedIndex = 2; + + final original = double.tryParse( + item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ?? + 0; + final current = + double.tryParse(item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0; + final discount = + original > 0 ? ((original - current) / original * 100).round() : 0; + + return Container( + decoration: BoxDecoration( + + borderRadius: BorderRadius.circular(20), + + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // ── Image area ── + Stack( + children: [ + + // Image or emoji fallback + GestureDetector( + onTap: () => showProductSheet(context, item), + + child: SizedBox( + height: 140, + width: double.infinity, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20)), + child: CachedNetworkImage( + imageUrl: item.imageUrl, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => _fallback(item), + ), + ), + ), + ), + + // Discount badge — top left + if (discount > 0) + Positioned( + top: 10, + left: 10, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF662582), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '$discount% OFF', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + ), + ), + + // Favourite — top right + Positioned( + top: 8, + right: 8, + child: FavButton( + size: 32, + initialValue: _isFav, + onChanged: (val) => setState(() => _isFav = val), + ), + ), + + // Add button — bottom right ON image + Positioned( + bottom: 10, + right: 10, + child: GestureDetector( + + onTap: () { + showModalBottomSheet( + + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + height: 420, + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Container( + width: 50, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(100), + ), + ), + + const SizedBox(height: 20), + + const Text( + "Choose Variant", + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF662582), + ), + ), + + const SizedBox(height: 3), + + Text( + "Scroll to select your size", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 13, + ), + ), + + const SizedBox(height: 20), + + Expanded( + child: PageView.builder( + controller: PageController( + viewportFraction: 0.30, + initialPage: selectedIndex, + ), + onPageChanged: (index) { + setModalState(() { + selectedIndex = index; + }); + }, + itemCount: 5, + itemBuilder: (context, index) { + final items = [ + { + "name": "Small", + "price": "₹149", + "image": + "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500", + }, + { + "name": "Medium", + "price": "₹199", + "image": + "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=500", + }, + { + "name": "Large", + "price": "₹249", + "image": + "https://images.unsplash.com/photo-1514326640560-7d063ef2aed5?w=500", + }, + { + "name": "XL", + "price": "₹299", + "image": + "https://images.unsplash.com/photo-1594007654729-407eedc4be65?w=500", + }, + { + "name": "Party", + "price": "₹399", + "image": + "https://images.unsplash.com/photo-1514326640560-7d063ef2aed5?w=500", + }, + ]; + + final isSelected = selectedIndex == index; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + transform: Matrix4.identity() + ..scale(isSelected ? 1.15 : 1.15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: isSelected ? 100 : 85, + height: isSelected ? 100 : 85, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? const Color(0xFF662582) + : Colors.transparent, + width: 3, + ), + + ), + child: ClipOval( + child: Image.network( + items[index]["image"]!, + fit: BoxFit.fill, + + ), + ), + ), + + + ], + ), + + const SizedBox(height: 15), + + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + style: TextStyle( + fontSize: isSelected ? 18 : 14, + fontWeight: FontWeight.bold, + color: isSelected + ? Colors.black + : Colors.grey.shade600, + ), + child: Text(items[index]["name"]!), + ), + + const SizedBox(height: 5), + + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + style: TextStyle( + color: const Color(0xFF662582), + fontWeight: FontWeight.w700, + fontSize: isSelected ? 16 : 12, + ), + child: Text(items[index]["price"]!), + ), + + const SizedBox(height: 10), + + + ], + ), + ); + }, + ), + ), + + + + + const SizedBox(height: 20), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF662582), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + onPressed: () {}, + child: const Text( + "Confirm Selection", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + }, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.add_rounded, + color: Color(0xFF662582), + size: 20, + ), + ), + ), + ), + ], + ), + + // ── Info area ── + Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // Category + Text( + item.category, + style: const TextStyle( + fontSize: 10.5, + color: Color(0xFF662582), + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 3), + + // Name + Text( + item.name, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 6), + + // Rating + Row( + children: [ + const Icon(Icons.star_rounded, + size: 13, color: Color(0xFFFFC107)), + const SizedBox(width: 3), + Text( + '${item.rating}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 3), + Text( + '(${item.reviewCount})', + style: const TextStyle( + fontSize: 10.5, + color: Color(0xFF9E9E9E), + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Price + Row( + children: [ + Text( + item.price, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 6), + if (discount > 0) + Text( + item.originalPrice, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF9E9E9E), + decoration: TextDecoration.lineThrough, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _fallback(ProductItem item) { + return Container( + height: 140, + width: double.infinity, + color: item.bgColor, + child: const Center( + child: Icon( + Icons.image_not_supported, + size: 40, + color: Colors.grey, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/view/product/refill_home_screen.dart b/lib/view/product/refill_home_screen.dart new file mode 100644 index 0000000..da67951 --- /dev/null +++ b/lib/view/product/refill_home_screen.dart @@ -0,0 +1,1228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + + +class AutoRefillDemo extends StatelessWidget { + const AutoRefillDemo({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Auto Refill', + debugShowCheckedModeBanner: false, + theme: ThemeData( + fontFamily: 'SF Pro Display', + scaffoldBackgroundColor: const Color(0xFFF5F5F7), + colorScheme: ColorScheme.fromSeed(seedColor: primaryColor), + useMaterial3: true, + ), + home: const RootNavigator(), + ); + } +} + +const Color primaryColor = Color(0xFF662582); + +// ─── Data Models ─────────────────────────────────────────────── + +class Product { + final int id; + final String name; + final String brand; + final String days; + final String nextLabel; + final String nextDate; + final String image; + bool enabled; + + Product({ + required this.id, + required this.name, + required this.brand, + required this.days, + required this.nextLabel, + required this.nextDate, + required this.image, + required this.enabled, + }); +} + +class TopPick { + final String name; + final String qty; + final String price; + final String image; + const TopPick({ + required this.name, + required this.qty, + required this.price, + required this.image, + }); +} + +final List sampleProducts = [ + Product( + id: 1, + name: 'Salt', + brand: '1 Kg • Tata Salt', + days: '2 days remaining', + nextLabel: 'Next order:', + nextDate: '15 Jul', + image: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?w=200', + enabled: true, + ), + Product( + id: 2, + name: 'Milk', + brand: '1 Litre • Amul', + days: 'Tomorrow', + nextLabel: 'Next delivery:', + nextDate: 'Tomorrow, 7 AM', + image: 'https://images.unsplash.com/photo-1550583724-b2692b85b150?w=200', + enabled: true, + ), + Product( + id: 3, + name: 'Vitamin Tablets', + brand: '60 Tablets • HealthVit', + days: '5 days remaining', + nextLabel: 'Next order:', + nextDate: '18 Jul', + image: 'https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=200', + enabled: true, + ), + Product( + id: 4, + name: 'Dog Food', + brand: '3 Kg • Pedigree', + days: '1 week remaining', + nextLabel: 'Next order:', + nextDate: '22 Jul', + image: 'https://images.unsplash.com/photo-1589924691995-400dc9a36ab6?w=200', + enabled: true, + ), +]; + +const List topPicks = [ + TopPick( + name: 'Sunflower Oil', + qty: '1 Litre', + price: '₹135', + image: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?w=200', + ), + TopPick( + name: 'Toor Dal', + qty: '1 Kg', + price: '₹110', + image: 'https://images.unsplash.com/photo-1585996989620-6d8e5e03a3b1?w=200', + ), + TopPick( + name: 'India Gate Basmati Rice', + qty: '5 Kg', + price: '₹399', + image: 'https://images.unsplash.com/photo-1536304993881-ff6e9eefa2a6?w=200', + ), + TopPick( + name: 'Aashirvaad Atta', + qty: '5 Kg', + price: '₹249', + image: 'https://images.unsplash.com/photo-1574323347407-f5e1ad6d020b?w=200', + ), +]; + +// ─── Root Navigator ───────────────────────────────────────────── + +class RootNavigator extends StatefulWidget { + const RootNavigator({super.key}); + + @override + State createState() => _RootNavigatorState(); +} + +class _RootNavigatorState extends State { + bool _showSetup = false; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, anim) => SlideTransition( + position: Tween( + begin: _showSetup ? const Offset(1, 0) : const Offset(-1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)), + child: child, + ), + child: _showSetup + ? AutoRefillSetupPage( + key: const ValueKey('setup'), + onBack: () => setState(() => _showSetup = false), + ) + : ForYouPage( + key: const ValueKey('home'), + onSetupTap: () => setState(() => _showSetup = true), + ), + ); + } +} + +// ─── For You Page ─────────────────────────────────────────────── + +class ForYouPage extends StatefulWidget { + final VoidCallback onSetupTap; + const ForYouPage({super.key, required this.onSetupTap}); + + @override + State createState() => _ForYouPageState(); +} + +class _ForYouPageState extends State { + late List _products; + + @override + void initState() { + super.initState(); + _products = List.from(sampleProducts); + } + + void _toggle(int id) { + setState(() { + final p = _products.firstWhere((e) => e.id == id); + p.enabled = !p.enabled; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F7), + appBar: AppBar( + backgroundColor: const Color(0xFFF5F5F7), + elevation: 0, + surfaceTintColor: Colors.transparent, + title: const Text( + 'For You', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 20, color: Color(0xFF111111)), + ), + + ), + body: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _HeroCard(), + const SizedBox(height: 24), + _SectionHeader(title: 'You may need soon', onViewAll: () {}), + const SizedBox(height: 14), + ..._products.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _ProductCard(product: p, onToggle: () => _toggle(p.id)), + ), + ), + const SizedBox(height: 10), + _SectionHeader(title: 'Top picks for you', onViewAll: () {}), + const SizedBox(height: 14), + SizedBox( + height: 210, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: topPicks.length, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (_, i) => _TopPickCard(pick: topPicks[i]), + ), + ), + const SizedBox(height: 18), + _NeverRunOutBanner(onSetupTap: widget.onSetupTap), + ], + ), + ), + ); + } +} + +// ─── Hero Card ────────────────────────────────────────────────── + +class _HeroCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 22, 20, 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(26), + gradient: const LinearGradient( + colors: [Color(0xFF5a1f72), primaryColor, Color(0xFF8b3ab0)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'For You', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 4), + const Text( + 'AI picks what matters for you', + style: TextStyle(color: Colors.white70, fontSize: 13), + ), + const SizedBox(height: 22), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: const [ + _StatItem(icon: Icons.shopping_bag_outlined, value: '5', label: 'Items to\nreorder'), + _StatItem(icon: Icons.calendar_today_outlined, value: '2', label: 'Upcoming\norders'), + _StatItem(icon: Icons.inventory_2_outlined, value: '1', label: 'Active\nsubscribed'), + _StatItem(icon: Icons.currency_rupee, value: '₹1,245', label: 'Est. spend\nthis month', smallValue: true), + ], + ), + ], + ), + ); + } +} + +class _StatItem extends StatelessWidget { + final IconData icon; + final String value; + final String label; + final bool smallValue; + + const _StatItem({ + required this.icon, + required this.value, + required this.label, + this.smallValue = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.18), + borderRadius: BorderRadius.circular(13), + ), + child: Icon(icon, color: Colors.white, size: 20), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + color: Colors.white, + fontSize: smallValue ? 15 : 18, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 3), + Text( + label, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white70, + fontSize: 10, + height: 1.35, + ), + ), + ], + ); + } +} + +// ─── Section Header ───────────────────────────────────────────── + +class _SectionHeader extends StatelessWidget { + final String title; + final VoidCallback onViewAll; + + const _SectionHeader({required this.title, required this.onViewAll}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFF111111), + ), + ), + GestureDetector( + onTap: onViewAll, + child: const Text( + 'View All ›', + style: TextStyle(color: primaryColor, fontSize: 13, fontWeight: FontWeight.w600), + ), + ), + ], + ); + } +} + +// ─── Product Card ─────────────────────────────────────────────── + +class _ProductCard extends StatelessWidget { + final Product product; + final VoidCallback onToggle; + + const _ProductCard({required this.product, required this.onToggle}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(12, 12, 14, 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + product.image, + width: 85, + height: 85, + fit: BoxFit.cover, + ), + ), + + const SizedBox(width: 14), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 12), + + Row( + children: [ + Expanded( + child: Text( + "ID: ${product.id}", + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: Text( + "Next: ${product.nextDate}", + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + + const SizedBox(height: 8), + + Row( + children: [ + Expanded( + child: Text( + "Days: ${product.days}", + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: RichText( + text: const TextSpan( + children: [ + TextSpan( + text: "Status: ", + style: TextStyle( + color: Colors.black87, + ), + ), + TextSpan( + text: "Active", + style: TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ) + ], + ) + ); + } +} + +// ─── iOS Toggle ───────────────────────────────────────────────── + +class _IOSToggle extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const _IOSToggle({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: 51, + height: 31, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(31), + color: value ? primaryColor : const Color(0xFFE5E5EA), + boxShadow: value + ? [] + : [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 0, spreadRadius: 1)], + ), + child: Padding( + padding: const EdgeInsets.all(2), + child: AnimatedAlign( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 27, + height: 27, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 6, offset: const Offset(0, 2)), + BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 1, offset: const Offset(0, 0.5)), + ], + ), + ), + ), + ), + ), + ); + } +} + +// ─── Top Pick Card ────────────────────────────────────────────── + +class _TopPickCard extends StatelessWidget { + final TopPick pick; + const _TopPickCard({required this.pick}); + + @override + Widget build(BuildContext context) { + return Container( + width: 118, + padding: const EdgeInsets.fromLTRB(10, 10, 10, 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 2)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + pick.image, + width: double.infinity, + height: 82, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + height: 82, + color: const Color(0xFFF0F0F0), + child: const Icon(Icons.image_not_supported_outlined, color: Colors.grey), + ), + ), + ), + const SizedBox(height: 9), + Text( + pick.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Color(0xFF111111), height: 1.35), + ), + const SizedBox(height: 2), + Text(pick.qty, style: const TextStyle(fontSize: 11, color: Color(0xFFAAAAAA))), + const SizedBox(height: 5), + Text(pick.price, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Color(0xFF111111))), + const SizedBox(height: 9), + GestureDetector( + onTap: () {}, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(9), + border: Border.all(color: primaryColor, width: 1.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Add', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: primaryColor)), + const SizedBox(width: 5), + Container( + width: 18, + height: 18, + decoration: const BoxDecoration(color: primaryColor, shape: BoxShape.circle), + child: const Icon(Icons.add, color: Colors.white, size: 13), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +// ─── Never Run Out Banner ─────────────────────────────────────── + +class _NeverRunOutBanner extends StatelessWidget { + final VoidCallback onSetupTap; + const _NeverRunOutBanner({required this.onSetupTap}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(18, 20, 18, 22), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)), + ], + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.12), + shape: BoxShape.circle, + ), + child: const Icon(Icons.autorenew_rounded, color: primaryColor, size: 26), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Never run out of essentials', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Color(0xFF111111)), + ), + SizedBox(height: 4), + Text( + 'We predict your needs and automatically reorder before you run out.', + style: TextStyle(fontSize: 12, color: Color(0xFF999999), height: 1.5), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(color: Color(0xFFF2F2F2), height: 1), + const SizedBox(height: 14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: const [ + _FeatureChip(icon: '✨', label: 'Smart prediction'), + _FeatureChip(icon: '🚚', label: 'Timely delivery'), + _FeatureChip(icon: '🔒', label: 'Secure auto pay'), + ], + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: onSetupTap, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + elevation: 0, + ), + child: const Text( + 'Setup Auto Refill', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: 0.2), + ), + ), + ), + ], + ), + ); + } +} + +class _FeatureChip extends StatelessWidget { + final String icon; + final String label; + const _FeatureChip({required this.icon, required this.label}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(icon, style: const TextStyle(fontSize: 18)), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF666666))), + ], + ); + } +} + +// ─── Auto Refill Setup Page ───────────────────────────────────── + +class AutoRefillSetupPage extends StatefulWidget { + final VoidCallback onBack; + const AutoRefillSetupPage({super.key, required this.onBack}); + + @override + State createState() => _AutoRefillSetupPageState(); +} + +class _AutoRefillSetupPageState extends State { + String _choice = 'auto'; + bool _autoPay = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.chevron_left, size: 28, color: Color(0xFF222222)), + onPressed: widget.onBack, + ), + title: const Text( + 'Auto Refill Setup', + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 17, color: Color(0xFF111111)), + ), + centerTitle: true, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFFDDDDDD), width: 1.5), + ), + child: const Center( + child: Text('?', style: TextStyle(fontWeight: FontWeight.w700, color: Color(0xFF888888), fontSize: 14)), + ), + ), + ), + ], + bottom: const PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider(color: Color(0xFFF5F5F5), height: 1), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Product row + Padding( + padding: const EdgeInsets.symmetric(vertical: 18), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + 'https://images.unsplash.com/photo-1586201375761-83865001e31c?w=200', + width: 74, + height: 74, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 14), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Salt', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: Color(0xFF111111), height: 1)), + SizedBox(height: 5), + Text('1 Kg • Tata Salt', style: TextStyle(fontSize: 13, color: Color(0xFF999999))), + SizedBox(height: 8), + Text('₹20', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Color(0xFF111111))), + ], + ), + ], + ), + ), + const Divider(color: Color(0xFFF0F0F0), height: 1), + const SizedBox(height: 22), + + // Step 1 + _StepLabel(step: 'Step 1:', label: 'Choose Refill Preference'), + const SizedBox(height: 14), + _RefillOption( + selected: _choice == 'auto', + onTap: () => setState(() => _choice = 'auto'), + title: 'Auto Refill (Recommended)', + subtitle: "We'll monitor your usage and reorder automatically before you run out.", + badge: 'Most popular', + trailing: null, + ), + const SizedBox(height: 10), + _RefillOption( + selected: _choice == 'reminder', + onTap: () => setState(() => _choice = 'reminder'), + title: 'Reminder Only', + subtitle: "We'll remind you when it's time to reorder.", + badge: null, + trailing: const Icon(Icons.notifications_outlined, size: 24, color: primaryColor), + ), + const SizedBox(height: 24), + + // Step 2 + _StepLabel(step: 'Step 2:', label: 'Set Preferences'), + const SizedBox(height: 6), + _PrefRow(icon: Icons.calendar_today_outlined, label: 'Order Frequency', value: 'Every 30 days'), + _PrefRow(icon: Icons.inventory_2_outlined, label: 'Quantity', value: '1 Kg'), + _PrefRow(icon: Icons.local_offer_outlined, label: 'Preferred Brand', value: 'Tata Salt'), + _PrefRowWithAction( + icon: Icons.store_outlined, + label: 'Preferred Store', + value: 'Zepto', + action: TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + backgroundColor: primaryColor.withOpacity(0.12), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text('Change', style: TextStyle(color: primaryColor, fontWeight: FontWeight.w700, fontSize: 12)), + ), + ), + const SizedBox(height: 24), + + // Step 3 + _StepLabel(step: 'Step 3:', label: 'Payment'), + const SizedBox(height: 14), + + // Auto Pay card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.shield_outlined, color: primaryColor, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('Auto Pay', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15, color: Color(0xFF111111))), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 2), + decoration: BoxDecoration(color: const Color(0xFF00C853), borderRadius: BorderRadius.circular(20)), + child: const Text('Secure', style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w700)), + ), + ], + ), + const SizedBox(height: 3), + const Text( + 'Amount will be automatically paid using your saved payment method.', + style: TextStyle(fontSize: 11, color: Color(0xFF999999), height: 1.5), + ), + ], + ), + ), + const SizedBox(width: 10), + _IOSToggle(value: _autoPay, onChanged: (v) => setState(() => _autoPay = v)), + ], + ), + ), + const SizedBox(height: 10), + + // UPI row + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 4)], + ), + child: const Center(child: Text('🏦', style: TextStyle(fontSize: 20))), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('UPI - AutoPay', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: Color(0xFF111111))), + SizedBox(height: 2), + Text('rahul@okaxis', style: TextStyle(fontSize: 12, color: Color(0xFF999999))), + ], + ), + ), + const Text('Change ›', style: TextStyle(color: primaryColor, fontSize: 13, fontWeight: FontWeight.w700)), + ], + ), + ), + const SizedBox(height: 22), + + // How it works + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.06), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: primaryColor.withOpacity(0.2), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'How Auto Refill Works', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 14, color: primaryColor), + ), + const SizedBox(height: 16), + ...[ + _HowItWorksData(Icons.calendar_today_outlined, 'We track your usage', 'Our AI monitors how quickly you use the product.'), + _HowItWorksData(Icons.auto_awesome_outlined, 'Predict & notify', "We predict when you'll run out and notify you."), + _HowItWorksData(Icons.shopping_cart_outlined, 'Auto order', 'We place the order before you run out.'), + _HowItWorksData(Icons.local_shipping_outlined, 'Auto pay & deliver', 'Payment is automatic and we deliver to you.'), + ].asMap().entries.map( + (entry) => Padding( + padding: EdgeInsets.only(bottom: entry.key < 3 ? 16 : 0), + child: _HowItWorksRow(data: entry.value), + ), + ), + ], + ), + ), + ], + ), + ), + + // Sticky CTA + bottomSheet: Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(16, 14, 16, 28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 0, + ), + child: const Text( + 'Enable Auto Refill', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, letterSpacing: 0.2), + ), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.lock_outline, size: 13, color: Color(0xFFAAAAAA)), + SizedBox(width: 5), + Text( + 'You can pause or cancel anytime', + style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA)), + ), + ], + ), + ], + ), + ), + ); + } +} + +// ─── Setup Page Sub-widgets ───────────────────────────────────── + +class _StepLabel extends StatelessWidget { + final String step; + final String label; + const _StepLabel({required this.step, required this.label}); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFF111111)), + children: [ + TextSpan(text: step, style: const TextStyle(color: primaryColor)), + TextSpan(text: ' $label'), + ], + ), + ); + } +} + +class _RefillOption extends StatelessWidget { + final bool selected; + final VoidCallback onTap; + final String title; + final String subtitle; + final String? badge; + final Widget? trailing; + + const _RefillOption({ + required this.selected, + required this.onTap, + required this.title, + required this.subtitle, + this.badge, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: selected ? primaryColor.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: selected ? primaryColor : const Color(0xFFEEEEEE), + width: 2, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: selected ? primaryColor : const Color(0xFFCCCCCC), + width: 2.5, + ), + color: selected ? primaryColor : Colors.white, + ), + child: selected + ? const Center( + child: CircleAvatar(radius: 4, backgroundColor: Colors.white), + ) + : null, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: Color(0xFF111111))), + if (badge != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 2), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.13), + borderRadius: BorderRadius.circular(20), + ), + child: Text(badge!, style: const TextStyle(color: primaryColor, fontSize: 10, fontWeight: FontWeight.w700)), + ), + ], + ), + const SizedBox(height: 5), + Text(subtitle, style: const TextStyle(fontSize: 12, color: Color(0xFF999999), height: 1.5)), + ], + ), + ), + if (trailing != null) ...[const SizedBox(width: 8), trailing!], + ], + ), + ), + ); + } +} + +class _PrefRow extends StatelessWidget { + final IconData icon; + final String label; + final String value; + const _PrefRow({required this.icon, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFF2F2F2)))), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration(color: const Color(0xFFF5F5F7), borderRadius: BorderRadius.circular(10)), + child: Icon(icon, size: 17, color: const Color(0xFF555555)), + ), + const SizedBox(width: 12), + Expanded(child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF333333), fontWeight: FontWeight.w500))), + Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), fontWeight: FontWeight.w600)), + const SizedBox(width: 6), + const Icon(Icons.keyboard_arrow_down, color: Color(0xFFBBBBBB), size: 18), + ], + ), + ); + } +} + +class _PrefRowWithAction extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final Widget action; + const _PrefRowWithAction({required this.icon, required this.label, required this.value, required this.action}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration(color: const Color(0xFFF5F5F7), borderRadius: BorderRadius.circular(10)), + child: Icon(icon, size: 17, color: const Color(0xFF555555)), + ), + const SizedBox(width: 12), + Expanded(child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF333333), fontWeight: FontWeight.w500))), + Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), fontWeight: FontWeight.w600)), + const SizedBox(width: 10), + action, + ], + ), + ); + } +} + +class _HowItWorksData { + final IconData icon; + final String title; + final String desc; + const _HowItWorksData(this.icon, this.title, this.desc); +} + +class _HowItWorksRow extends StatelessWidget { + final _HowItWorksData data; + const _HowItWorksRow({required this.data}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 4)], + ), + child: Icon(data.icon, size: 18, color: primaryColor), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.title, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13, color: Color(0xFF111111))), + const SizedBox(height: 3), + Text(data.desc, style: const TextStyle(fontSize: 12, color: Color(0xFF888888), height: 1.5)), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/view/product/tenant_products.dart b/lib/view/product/tenant_products.dart index 917d01e..45901b5 100644 --- a/lib/view/product/tenant_products.dart +++ b/lib/view/product/tenant_products.dart @@ -14,6 +14,7 @@ import '../../controllers/product/product_controller.dart'; import '../../controllers/product/variant_controller.dart'; import '../../domain/provider/varient/varient_pro.dart'; import '../../modules/product/product.dart'; +import '../../service/firebase_analytics/analytics_service.dart'; import '../../widgets/text_widget.dart'; import '../cart/cart_view.dart'; import '../home_view.dart'; @@ -65,7 +66,7 @@ class ProductsScreen extends StatelessWidget { /// Grid aspect ratio: slightly taller cards work better on a 3-col tablet grid. double _childAspectRatio(BuildContext context) => - _isTablet(context) ? 0.72 : 0.70; + _isTablet(context) ? 0.66 : 0.65; /// Category chip width: wider on tablet. double _chipWidth(BuildContext context) => _isTablet(context) ? 170.0 : 140.0; @@ -91,527 +92,617 @@ class ProductsScreen extends StatelessWidget { return AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarColor: Colors.white, - statusBarIconBrightness: Brightness.dark, // Android + statusBarIconBrightness: Brightness.light, // Android statusBarBrightness: Brightness.light, // iOS ), - child: Scaffold( - floatingActionButton: Obx(() { - - if (cartController.totalItems == 0) { - return const SizedBox(); // Hide FAB when cart is empty - } - - return FloatingActionButton.extended( - onPressed: () { - - Get.to(CartPage()); - - }, - backgroundColor: const Color(0xFF662582), - icon: const Icon(Icons.shopping_cart, color: Colors.white), - label: Text( - "View Cart (${cartController.totalItems})", - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); - }), backgroundColor: Colors.white, - appBar: AppBar( - surfaceTintColor: Colors.transparent, - - scrolledUnderElevation: 0, - animateColor: false, - elevation: 0, - backgroundColor: Color(0xFF662582), - automaticallyImplyLeading: false, - title: Obx(() { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, anim) => SizeTransition( - sizeFactor: anim, axis: Axis.horizontal, child: child), - child: controller.isSearching.value - ? Container( - key: const ValueKey("searchBar"), - height: 45, - decoration: BoxDecoration( + child: SafeArea( + child: Scaffold( + floatingActionButton: Obx(() { + + if (cartController.totalItems == 0) { + return const SizedBox(); // Hide FAB when cart is empty + } + + return FloatingActionButton.extended( + onPressed: () { + + Get.to(CartPage()); + + }, + backgroundColor: const Color(0xFF662582), + icon: const Icon(Icons.shopping_cart, color: Colors.white), + label: Text( + "View Cart (${cartController.totalItems})", + style: const TextStyle( color: Colors.white, - borderRadius: BorderRadius.circular(25), + fontWeight: FontWeight.bold, ), - child: TextField( - autofocus: true, - onChanged: (value) => - controller.searchQuery.value = value, - decoration: InputDecoration( - hintText: "Search products...", - hintStyle: TextStyle( - color: Colors.grey.shade500, fontSize: 14), - prefixIcon: const Icon(Icons.search, - color: Colors.deepPurple), - border: InputBorder.none, - contentPadding: - const EdgeInsets.symmetric(vertical: 10), - ), - ), - ) - : Row( - key: const ValueKey("tenantInfo"), - children: [ - IconButton( - icon: const Icon(Icons.arrow_back,color: Colors.white,), - onPressed: () => Navigator.pop(context), - ), - const SizedBox(width: 4), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ReusableTextWidget( - text: tenantName, - color: Colors.white, - fontFamily: FontConstants.fontFamily, - // ✅ Slightly larger on tablet - fontSize: isTablet ? 18 : 16, - fontWeight: FontWeight.w700, - textAlign: TextAlign.start, - maxLines: 1, - ), - ReusableTextWidget( - text: locationname, - color: Colors.white, - fontFamily: FontConstants.fontFamily, - // ✅ Slightly larger on tablet - fontSize: isTablet ? 15 : 13, - fontWeight: FontWeight.w500, - textAlign: TextAlign.start, - maxLines: 1, - ), - ], - ), - ], ), ); - }), - actions: [ - Obx(() { - return controller.isSearching.value - ? IconButton( - icon: - const Icon(Icons.close, color: Colors.redAccent), - onPressed: () { - controller.isSearching.value = false; - controller.searchQuery.value = ''; - }, - ) - : IconButton( - icon: const Icon(Icons.search, - color: Colors.white), - onPressed: () => - controller.isSearching.value = true, - ); - }), - ], - ), - body: Stack( - children: [ - Obx(() { - if (!controller.isConnected.value) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.wifi_off, size: 80, color: Colors.grey), - SizedBox(height: 16), - ReusableTextWidget( - text: 'No Internet Connection', - color: Colors.grey[700]!, - fontFamily: FontConstants.fontFamily, - fontSize: 18, - fontWeight: FontWeight.w600, - textAlign: TextAlign.center, - ), - ], + }), backgroundColor: Colors.white, + appBar: AppBar( + surfaceTintColor: Colors.transparent, + + scrolledUnderElevation: 0, + animateColor: false, + elevation: 0, + backgroundColor: Color(0xFF662582), + automaticallyImplyLeading: false, + title: Obx(() { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, anim) => SizeTransition( + sizeFactor: anim, axis: Axis.horizontal, child: child), + child: controller.isSearching.value + ? Container( + key: const ValueKey("searchBar"), + height: 45, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), ), - ); - } - - if (controller.isLoading.value) { - return productsShimmer(context); - } - - final details = - controller.productResponse.value?.data?.details ?? []; - if (details.isEmpty) { - controller.fetchProducts(categoryId, tenantId, tenantloc); - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 50), - Image.asset( - AssetConstants.noDataProducts, - height: 100, - width: 130, - fit: BoxFit.fill, - ), - ReusableTextWidget( - text: 'No Products Yet', - color: ColorConstants.blackColor, - fontFamily: FontConstants.fontFamily, - fontSize: 18, - fontWeight: FontWeight.w700, - maxLines: 2, - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - return Column( - children: [ - - SizedBox( - height: isTablet ? 200 : 120, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFF662582), - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(40), - bottomLeft: Radius.circular(40) - ) - ), - - child: Obx(() { - final details = - controller.productResponse.value?.data?.details ?? []; - if (details.isEmpty) return const SizedBox(); - - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 12), - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - itemCount: details.length, - separatorBuilder: (_, __) => const SizedBox(width: 6), - itemBuilder: (context, index) { - final sub = details[index]; - - return TweenAnimationBuilder( - duration: Duration(milliseconds: 300 + (index * 50)), - tween: Tween(begin: 0.0, end: 1.0), - curve: Curves.easeOutCubic, - builder: (context, animValue, child) { - return Transform.translate( - offset: Offset(0, 10 * (1 - animValue)), - child: Opacity(opacity: animValue, child: child), - ); - }, - child: Obx(() { - final isSelected = - controller.selectedIndex.value == index; - - return GestureDetector( - onTap: () { - controller.selectedIndex.value = index; - controller.searchQuery.value = ''; - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOutCubic, - - // ✅ FIXED WIDTH FOR ALL - width: isTablet ? 100 : 80, - - padding: EdgeInsets.symmetric( - horizontal: 6, - vertical: isTablet ? 10 : 8, - ), - - decoration: BoxDecoration( - color: Colors.transparent, - // border: Border( - // bottom: BorderSide( - // color: isSelected - // ? Colors.white - // : Colors.transparent, - // width: 3.5 - // ), - // ), - ), - - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 🔥 ICON - AnimatedOpacity( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - opacity: isSelected ? 1.0 : 1.0, - child: AnimatedScale( - duration: const Duration(milliseconds: 250), - scale: isSelected ? 1.0 : 0.9, - child: Container( - height: 60, - width: 60, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade200, // background - ), - child: ClipOval( - child: Image.network( - sub.image ?? '', - fit: BoxFit.cover, // 🔥 IMPORTANT for circle fill - - loadingBuilder: (context, child, progress) { - if (progress == null) return child; - return const Center( - child: CircularProgressIndicator(strokeWidth: 2), - ); - }, - - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon( - Icons.image_not_supported, - color: Colors.grey, - ), - ); - }, - ), - ), - ), - ), - ), - - const SizedBox(height: 6), - - // 🔥 TEXT (FIXED + ELLIPSIS) - SizedBox( - width: double.infinity, - child: AnimatedDefaultTextStyle( - duration: - const Duration(milliseconds: 200), - style: TextStyle( - fontSize: isTablet - ? (isSelected ? 13 : 12) - : (isSelected ? 12 : 11), - fontWeight: isSelected - ? FontWeight.w700 - : FontWeight.w500, - color: isSelected - ? Colors.white - : Colors.white60, - ), - child: Text( - sub.subcategoryname ?? "", - overflow: TextOverflow.ellipsis, - maxLines: 1, - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - ), - ); - }), - ); - }, - ); - }), + child: TextField( + autofocus: true, + onChanged: (value) => + controller.searchQuery.value = value, + decoration: InputDecoration( + hintText: "Search products...", + hintStyle: TextStyle( + color: Colors.grey.shade500, fontSize: 14), + prefixIcon: const Icon(Icons.search, + color: Colors.deepPurple), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(vertical: 10), ), ), - - // ── Product grid ───────────────────────────────────── - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - // ✅ Responsive columns & image height - final crossAxisCount = _crossAxisCount(context); - final itemWidth = - constraints.maxWidth / crossAxisCount; - final imageHeight = itemWidth * 0.75; - - double scaleButtonWidth(double w) => w * 0.5; - double scaleButtonHeight(double h) => h * 0.06; - - return Obx(() { - final products = controller.filteredProducts; - if (products.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Lottie.asset( - 'assets/lotties/empty.json', - width: 200, - height: 200, - fit: BoxFit.contain, - ), - const SizedBox(height: 20), - const Text( - "No products found", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500), - ), - ], - ), - ); - } - - return GridView.builder( - padding: const EdgeInsets.all(12), - itemCount: controller.filteredProducts.length, - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - // ✅ 3 columns on tablet, 2 on phone - crossAxisCount: crossAxisCount, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - childAspectRatio: - _childAspectRatio(context), - ), + ) + : Row( + key: const ValueKey("tenantInfo"), + children: [ + IconButton( + icon: const Icon(Icons.arrow_back,color: Colors.white,), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 4), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: tenantName, + color: Colors.white, + fontFamily: FontConstants.fontFamily, + // ✅ Slightly larger on tablet + fontSize: isTablet ? 18 : 16, + fontWeight: FontWeight.w700, + textAlign: TextAlign.start, + maxLines: 1, + ), + ReusableTextWidget( + text: locationname, + color: Colors.white, + fontFamily: FontConstants.fontFamily, + // ✅ Slightly larger on tablet + fontSize: isTablet ? 15 : 13, + fontWeight: FontWeight.w500, + textAlign: TextAlign.start, + maxLines: 1, + ), + ], + ), + ], + ), + ); + }), + actions: [ + Obx(() { + return controller.isSearching.value + ? IconButton( + icon: + const Icon(Icons.close, color: Colors.redAccent), + onPressed: () { + controller.isSearching.value = false; + controller.searchQuery.value = ''; + }, + ) + : IconButton( + icon: const Icon(Icons.search, + color: Colors.white), + onPressed: () => + controller.isSearching.value = true, + ); + }), + ], + ), + body: Stack( + children: [ + Obx(() { + if (!controller.isConnected.value) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.wifi_off, size: 80, color: Colors.grey), + SizedBox(height: 16), + ReusableTextWidget( + text: 'No Internet Connection', + color: Colors.grey[700]!, + fontFamily: FontConstants.fontFamily, + fontSize: 18, + fontWeight: FontWeight.w600, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + if (controller.isLoading.value) { + return productsShimmer(context); + } + + final details = + controller.productResponse.value?.data?.details ?? []; + if (details.isEmpty) { + controller.fetchProducts(categoryId, tenantId, tenantloc); + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 50), + Image.asset( + AssetConstants.noDataProducts, + height: 100, + width: 130, + fit: BoxFit.fill, + ), + ReusableTextWidget( + text: 'No Products Yet', + color: ColorConstants.blackColor, + fontFamily: FontConstants.fontFamily, + fontSize: 18, + fontWeight: FontWeight.w700, + maxLines: 2, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return Column( + children: [ + + SizedBox( + height: isTablet ? 200 : 120, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF662582), + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(40), + bottomLeft: Radius.circular(40) + ) + ), + + child: Obx(() { + final details = + controller.productResponse.value?.data?.details ?? []; + if (details.isEmpty) return const SizedBox(); + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: details.length, + separatorBuilder: (_, __) => const SizedBox(width: 6), itemBuilder: (context, index) { - final product = - controller.filteredProducts[index]; - print(product); - final status = product.productstatus - ?.toString() - .toUpperCase() ?? - ""; - final inStock = - status.contains("ACTIVE") || - status.contains("AVAILABLE"); - print(inStock); - - return GestureDetector( - onTap: () { - Get.to(() => ProductViewPage( - product: product, - tenantImage: tenantImage, - tenantName: tenantName, - tenantId: tenantId, - locationId: locationId, - )); - + final sub = details[index]; + + return TweenAnimationBuilder( + duration: Duration(milliseconds: 300 + (index * 50)), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOutCubic, + builder: (context, animValue, child) { + return Transform.translate( + offset: Offset(0, 10 * (1 - animValue)), + child: Opacity(opacity: animValue, child: child), + ); }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(7), - border: Border.all( - color: Colors.black12, - width: 0.20), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: const Offset(2, 2), + child: Obx(() { + final isSelected = + controller.selectedIndex.value == index; + + return GestureDetector( + onTap: () { + controller.selectedIndex.value = index; + controller.searchQuery.value = ''; + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOutCubic, + + // ✅ FIXED WIDTH FOR ALL + width: isTablet ? 100 : 80, + + padding: EdgeInsets.symmetric( + horizontal: 6, + vertical: isTablet ? 10 : 8, ), - ], - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // 🔹 Product Image - Container( - margin: - const EdgeInsets.symmetric( - horizontal: 5, - vertical: 5), - child: ClipRRect( - borderRadius: - const BorderRadius.only( - bottomLeft: - Radius.circular(17), - bottomRight: - Radius.circular(17), - topRight: Radius.circular(7), - topLeft: Radius.circular(7), - ), - child: Stack( - alignment: Alignment.center, - children: [ - ColorFiltered( - colorFilter: inStock - ? const ColorFilter - .mode( - Colors.transparent, - BlendMode.multiply, - ) - : const ColorFilter - .matrix([ - 0.2126, 0.7152, - 0.0722, 0, 0, - 0.2126, 0.7152, - 0.0722, 0, 0, - 0.2126, 0.7152, - 0.0722, 0, 0, - 0, 0, 0, 1, 0, - ]), - child: Image.network( - product.productimage ?? - '', - // ✅ imageHeight is now - // computed per column count - height: imageHeight, - width: double.infinity, - fit: BoxFit.cover, - loadingBuilder: (context, - child, - loadingProgress) { - if (loadingProgress == - null) return child; - return Shimmer - .fromColors( - baseColor: Colors - .grey[300]!, - highlightColor: - Colors - .grey[100]!, - child: Container( - height: - imageHeight, - color: - Colors.white, - ), - ); - }, - errorBuilder: (context, - error, stackTrace) { - return Container( - height: imageHeight, - width: - double.infinity, - color: - Colors.grey[300], - child: const Icon( - Icons.image, - size: 48, - color: - Colors.grey), - ); - }, + + decoration: BoxDecoration( + color: Colors.transparent, + // border: Border( + // bottom: BorderSide( + // color: isSelected + // ? Colors.white + // : Colors.transparent, + // width: 3.5 + // ), + // ), + ), + + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 🔥 ICON + AnimatedOpacity( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + opacity: isSelected ? 1.0 : 1.0, + child: AnimatedScale( + duration: const Duration(milliseconds: 250), + scale: isSelected ? 1.0 : 0.9, + child: Container( + height: 60, + width: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade200, // background + ), + child: ClipOval( + child: Image.network( + sub.image ?? '', + fit: BoxFit.cover, // 🔥 IMPORTANT for circle fill + + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const Center( + child: CircularProgressIndicator(strokeWidth: 2), + ); + }, + + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + ), ), ), - - if (!inStock) - Container( - height: imageHeight, - width: double.infinity, - color: Colors.grey - .withOpacity(0.10), + ), + ), + + const SizedBox(height: 6), + + // 🔥 TEXT (FIXED + ELLIPSIS) + SizedBox( + width: double.infinity, + child: AnimatedDefaultTextStyle( + duration: + const Duration(milliseconds: 200), + style: TextStyle( + fontSize: isTablet + ? (isSelected ? 13 : 12) + : (isSelected ? 12 : 11), + fontWeight: isSelected + ? FontWeight.w700 + : FontWeight.w500, + color: isSelected + ? Colors.white + : Colors.white60, + ), + child: Text( + sub.subcategoryname ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + ); + }), + ); + }, + ); + }), + ), + ), + + // ── Product grid ───────────────────────────────────── + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + // ✅ Responsive columns & image height + final crossAxisCount = _crossAxisCount(context); + final itemWidth = + constraints.maxWidth / crossAxisCount; + final imageHeight = itemWidth * 0.75; + + double scaleButtonWidth(double w) => w * 0.5; + double scaleButtonHeight(double h) => h * 0.06; + + return Obx(() { + final products = controller.filteredProducts; + if (products.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Lottie.asset( + 'assets/lotties/empty.json', + width: 200, + height: 200, + fit: BoxFit.contain, + ), + const SizedBox(height: 20), + const Text( + "No products found", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500), + ), + ], + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.all(12), + itemCount: controller.filteredProducts.length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + // ✅ 3 columns on tablet, 2 on phone + crossAxisCount: crossAxisCount, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: + _childAspectRatio(context), + ), + itemBuilder: (context, index) { + final product = + controller.filteredProducts[index]; + print(product); + final status = product.productstatus + ?.toString() + .toUpperCase() ?? + ""; + final inStock = + status.contains("ACTIVE") || + status.contains("AVAILABLE"); + print(inStock); + + return GestureDetector( + onTap: () async { + + await AnalyticsService.logEvent( + 'product_viewed', + parameters: { + 'product_id': product.productid ?? 0, + 'product_name': product.productname ?? '', + 'tenant_id': product.tenantid ?? 0, + }, + ); + + Get.to(() => ProductViewPage( + product: product, + tenantImage: tenantImage, + tenantName: tenantName, + tenantId: tenantId, + locationId: locationId, + )); + + }, + child: Container( + decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: + // BorderRadius.circular(7), + // border: Border.all( + // color: Colors.black12, + // width: 0.20), + // boxShadow: [ + // BoxShadow( + // color: Colors.black12, + // blurRadius: 6, + // offset: const Offset(2, 2), + // ), + // ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // 🔹 Product Image + Container( + margin: + const EdgeInsets.symmetric( + horizontal: 5, + vertical: 5), + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(17)), + child: Stack( + alignment: Alignment.center, + children: [ + ColorFiltered( + colorFilter: inStock + ? const ColorFilter + .mode( + Colors.transparent, + BlendMode.multiply, + ) + : const ColorFilter + .matrix([ + 0.2126, 0.7152, + 0.0722, 0, 0, + 0.2126, 0.7152, + 0.0722, 0, 0, + 0.2126, 0.7152, + 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]), + child: Image.network( + product.productimage ?? + '', + // ✅ imageHeight is now + // computed per column count + height: imageHeight, + width: double.infinity, + fit: BoxFit.cover, + loadingBuilder: (context, + child, + loadingProgress) { + if (loadingProgress == + null) return child; + return Shimmer + .fromColors( + baseColor: Colors + .grey[300]!, + highlightColor: + Colors + .grey[100]!, + child: Container( + height: + imageHeight, + color: + Colors.white, + ), + ); + }, + errorBuilder: (context, + error, stackTrace) { + return Container( + height: imageHeight, + width: + double.infinity, + color: + Colors.grey[300], + child: const Icon( + Icons.image, + size: 48, + color: + Colors.grey), + ); + }, + ), ), - + + if (!inStock) + Container( + height: imageHeight, + width: double.infinity, + color: Colors.grey + .withOpacity(0.10), + ), + + + ], + ), + ), + ), + + // 🔹 Product Info + Padding( + padding: const EdgeInsets + .symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ReusableTextWidget( + text: product.productname ?? + "No name", + color: Colors.black + .withOpacity(0.7), + fontFamily: + FontConstants.fontFamily, + // ✅ Scaled font + fontSize: _scaleFont( + context, 14), + fontWeight: FontWeight.bold, + textAlign: TextAlign.start, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + + + + + + const SizedBox(height: 4), + + Row( + children: [ + Flexible( + child: ReusableTextWidget( + text: + "₹${(product.productcost ?? 0) - (product.discount ?? 0)}", + color: Colors.black + .withOpacity(0.7), + fontFamily: FontConstants + .fontFamily, + fontSize: _scaleFont( + context, 15), + fontWeight: + FontWeight.w600, + textAlign: + TextAlign.start, + ), + ), + + SizedBox(width: 5), + + if ((product.discount ?? + 0) > + 0) + Flexible( + child: + ReusableTextWidget( + text: + "₹${product.productcost ?? 0}", + color: Colors.red, + fontFamily: + FontConstants + .fontFamily, + fontSize: _scaleFont( + context, 15), + fontWeight: + FontWeight.w600, + textAlign: + TextAlign.start, + isUnderText: + TextDecoration + .lineThrough, + ), + ), + + + const SizedBox(width: 5), + ], + ), if (!inStock) Text( 'Out of stock', style: TextStyle( - color: Colors.white, + color: Colors.red, // ✅ Slightly larger // OOS label on tablet fontSize: isTablet @@ -622,589 +713,522 @@ class ProductsScreen extends StatelessWidget { letterSpacing: 0.4, ), ), + ], ), ), - ), - - // 🔹 Product Info - Padding( - padding: const EdgeInsets - .symmetric(horizontal: 10), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ReusableTextWidget( - text: product.productname ?? - "No name", - color: Colors.black - .withOpacity(0.7), - fontFamily: - FontConstants.fontFamily, - // ✅ Scaled font - fontSize: _scaleFont( - context, 14), - fontWeight: FontWeight.bold, - textAlign: TextAlign.start, - maxLines: 1, - overflow: - TextOverflow.ellipsis, - ), - - const SizedBox(height: 4), - - Row( - children: [ - Flexible( - child: ReusableTextWidget( - text: - "₹${(product.productcost ?? 0) - (product.discount ?? 0)}", - color: Colors.black - .withOpacity(0.7), - fontFamily: FontConstants - .fontFamily, - fontSize: _scaleFont( - context, 15), - fontWeight: - FontWeight.w600, - textAlign: - TextAlign.start, + + // 🔹 Bottom Row — More Details + Add Button + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: InkWell( + onTap: () async { + + await AnalyticsService.logEvent( + 'product_viewed', + parameters: { + 'product_id': product.productid ?? 0, + 'product_name': product.productname ?? '', + 'tenant_id': product.tenantid ?? 0, + }, + ); + + Get.to(() => + ProductViewPage( + product: product, + tenantImage: + tenantImage, + tenantName: + tenantName, + tenantId: tenantId, + locationId: + locationId, + )); + }, + child: Row( + children: [ + ReusableTextWidget( + text: + "${product.unitvalue ?? 0} ${productunitValues.reverse[product.productunit ?? 0] ?? ''}", + color: Colors.black + .withOpacity(0.7), + fontFamily: + FontConstants + .fontFamily, + fontSize: _scaleFont( + context, 14), + fontWeight: + FontWeight.w500, + textAlign: + TextAlign.start, + ), + const SizedBox( + width: 3), + const Icon( + Icons + .arrow_forward_ios, + size: 12, + color: Colors.grey), + ], ), ), - - SizedBox(width: 5), - - if ((product.discount ?? - 0) > - 0) - Flexible( - child: - ReusableTextWidget( - text: - "₹${product.productcost ?? 0}", - color: Colors.red, - fontFamily: - FontConstants - .fontFamily, - fontSize: _scaleFont( - context, 15), - fontWeight: - FontWeight.w600, - textAlign: - TextAlign.start, - isUnderText: - TextDecoration - .lineThrough, - ), - ), - - const SizedBox(width: 5), - ], - ), - ], - ), - ), - - // 🔹 Bottom Row — More Details + Add Button - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 6), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.center, - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Expanded( - child: InkWell( - onTap: () { - Get.to(() => - ProductViewPage( - product: product, - tenantImage: - tenantImage, - tenantName: - tenantName, - tenantId: tenantId, - locationId: - locationId, - )); - }, - child: Row( - children: [ - ReusableTextWidget( - text: - "${product.unitvalue ?? 0} ${productunitValues.reverse[product.productunit ?? 0] ?? ''}", - color: Colors.black - .withOpacity(0.7), - fontFamily: - FontConstants - .fontFamily, - fontSize: _scaleFont( - context, 14), - fontWeight: - FontWeight.w500, - textAlign: - TextAlign.start, - ), - const SizedBox( - width: 3), - const Icon( - Icons - .arrow_forward_ios, - size: 12, - color: Colors.grey), - ], - ), ), - ), - const SizedBox(width: 6), - - // Add button - InkWell( - onTap: () async { - if (inStock) { - variantController - .clearVariantState(); - await variantController - .fetchVariants( - tenantId: - product.tenantid!, - variantId: - product.variants!, - ); - - Get.bottomSheet( - Container( - decoration: - BoxDecoration( - color: Colors - .grey.shade200, - borderRadius: - const BorderRadius - .vertical( - top: Radius - .circular(12), + const SizedBox(width: 6), + + // Add button + InkWell( + onTap: () async { + if (inStock) { + variantController + .clearVariantState(); + await variantController + .fetchVariants( + tenantId: + product.tenantid!, + variantId: + product.variants!, + ); + + Get.bottomSheet( + Container( + decoration: + BoxDecoration( + color: Colors + .grey.shade200, + borderRadius: + const BorderRadius + .vertical( + top: Radius + .circular(12), + ), ), - ), - padding: - const EdgeInsets - .all(16), - child: Column( - mainAxisSize: - MainAxisSize - .min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - product - .productname!, - style: TextStyle( - fontSize: isTablet - ? 21 - : 19, - fontWeight: - FontWeight.w700), - ), - IconButton( - onPressed: - () { - Navigator.of( - context) - .pop(); - }, - icon: - const Icon( - Icons - .close), - ), - ], - ), - const SizedBox( - height: 20), - - Flexible( - child: Obx(() { - if (variantController - .isLoading - .value) { - return const SizedBox( - height: - 200, - child: Center( - child: - CircularProgressIndicator()), - ); - } - - if (variantController - .productVariants - .isEmpty) { - return const SizedBox( - height: - 100, - child: Center( - child: Text( - "No variants found")), - ); - } - - return ListView - .builder( - shrinkWrap: - true, - itemCount: - variantController - .productVariants - .length, - itemBuilder: - (context, - index) { - final variant = - variantController - .productVariants[index]; - - return Obx( - () { - final selectedId = - variantController - .selectedProductId - .value; - final isSelected = - selectedId == - variant.productid; - - return Container( - margin: const EdgeInsets - .symmetric( - vertical: - 6), - padding: const EdgeInsets - .all( - 12), - decoration: - BoxDecoration( - color: - Colors.white, - borderRadius: - BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - variant.productimage ?? '', - // ✅ Bigger variant thumbnails on tablet - height: isTablet ? 80 : 65, - width: isTablet ? 80 : 65, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( - height: isTablet ? 80 : 65, - width: isTablet ? 80 : 65, - color: Colors.grey[300], - child: const Icon(Icons.image, color: Colors.grey), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - variant.productname ?? '', - style: TextStyle( - // ✅ Larger variant name on tablet - fontSize: isTablet ? 18 : 16, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "₹${((variant.productcost ?? 0) - (variant.discount ?? 0)).toStringAsFixed(1)}", - style: TextStyle( - fontSize: isTablet ? 17 : 15, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(width: 8), - if ((variant.discount ?? 0) > 0) - Text( - "₹${variant.productcost!.toInt()}", - style: TextStyle( - fontSize: isTablet ? 17 : 15, - fontWeight: FontWeight.bold, - color: Colors.red, - decoration: TextDecoration.lineThrough, - ), - ), - ], - ), - ], - ), - ), - Checkbox( - value: - isSelected, - onChanged: - (val) { - variantController.selectVariant(variant.productid!); - }, - ), - ], - ), - ); - }); - }, - ); - }), - ), - const SizedBox( - height: 20), - - Obx(() { - final selectedId = - variantController - .selectedProductId - .value; - if (selectedId == - null) - return const SizedBox - .shrink(); - - final variant = - variantController - .productVariants - .firstWhere( - (v) => - v.productid == - selectedId); - final variantStatus = - variant.productstatus - ?.toString() - .toUpperCase() ?? - ""; - final variantInStock = - variantStatus.contains( - "ACTIVE") || - variantStatus - .contains( - "AVAILABLE"); - - final qty = - variantController - .variantQuantities[ - selectedId] ?? - 1; - - return Row( + padding: + const EdgeInsets + .all(16), + child: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ - Row( - children: [ - IconButton( - icon: const Icon( - Icons - .remove, - color: Colors - .redAccent), - onPressed: variantInStock - ? () => variantController.decreaseQuantity(selectedId) - : null, - ), - Text( - "$qty", - style: TextStyle( - fontSize: isTablet - ? 18 - : 16, - fontWeight: - FontWeight.bold), - ), - IconButton( - icon: const Icon( - Icons - .add, - color: Colors - .green), - onPressed: variantInStock - ? () => variantController.increaseQuantity(selectedId) - : null, - ), - ], + Text( + product + .productname!, + style: TextStyle( + fontSize: isTablet + ? 21 + : 19, + fontWeight: + FontWeight.w700), ), - SizedBox( - width: scaleButtonWidth( - MediaQuery.of(context) - .size - .width), - height: scaleButtonHeight( - MediaQuery.of(context) - .size - .height), - child: - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: variantInStock ? ColorConstants.primaryColor : Colors.grey.shade400, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + IconButton( + onPressed: + () { + Navigator.of( + context) + .pop(); + }, + icon: + const Icon( + Icons + .close), + ), + ], + ), + const SizedBox( + height: 20), + + Flexible( + child: Obx(() { + if (variantController + .isLoading + .value) { + return const SizedBox( + height: + 200, + child: Center( + child: + CircularProgressIndicator()), + ); + } + + if (variantController + .productVariants + .isEmpty) { + return const SizedBox( + height: + 100, + child: Center( + child: Text( + "No variants found")), + ); + } + + return ListView + .builder( + shrinkWrap: + true, + itemCount: + variantController + .productVariants + .length, + itemBuilder: + (context, + index) { + final variant = + variantController + .productVariants[index]; + + return Obx( + () { + final selectedId = + variantController + .selectedProductId + .value; + final isSelected = + selectedId == + variant.productid; + + return Container( + margin: const EdgeInsets + .symmetric( + vertical: + 6), + padding: const EdgeInsets + .all( + 12), + decoration: + BoxDecoration( + color: + Colors.white, + borderRadius: + BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + offset: const Offset(0, 3), + ), + ], + ), + child: + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + variant.productimage ?? '', + // ✅ Bigger variant thumbnails on tablet + height: isTablet ? 80 : 65, + width: isTablet ? 80 : 65, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + height: isTablet ? 80 : 65, + width: isTablet ? 80 : 65, + color: Colors.grey[300], + child: const Icon(Icons.image, color: Colors.grey), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + variant.productname ?? '', + style: TextStyle( + // ✅ Larger variant name on tablet + fontSize: isTablet ? 18 : 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "₹${((variant.productcost ?? 0) - (variant.discount ?? 0)).toStringAsFixed(1)}", + style: TextStyle( + fontSize: isTablet ? 17 : 15, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(width: 8), + if ((variant.discount ?? 0) > 0) + Text( + "₹${variant.productcost!.toInt()}", + style: TextStyle( + fontSize: isTablet ? 17 : 15, + fontWeight: FontWeight.bold, + color: Colors.red, + decoration: TextDecoration.lineThrough, + ), + ), + ], + ), + ], + ), + ), + Checkbox( + value: + isSelected, + onChanged: + (val) { + variantController.selectVariant(variant.productid!); + }, + ), + ], + ), + ); + }); + }, + ); + }), + ), + const SizedBox( + height: 20), + + Obx(() { + final selectedId = + variantController + .selectedProductId + .value; + if (selectedId == + null) + return const SizedBox + .shrink(); + + final variant = + variantController + .productVariants + .firstWhere( + (v) => + v.productid == + selectedId); + final variantStatus = + variant.productstatus + ?.toString() + .toUpperCase() ?? + ""; + final variantInStock = + variantStatus.contains( + "ACTIVE") || + variantStatus + .contains( + "AVAILABLE"); + + final qty = + variantController + .variantQuantities[ + selectedId] ?? + 1; + + return Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + IconButton( + icon: const Icon( + Icons + .remove, + color: Colors + .redAccent), + onPressed: variantInStock + ? () => variantController.decreaseQuantity(selectedId) + : null, ), - ), - onPressed: - variantInStock - ? () { - final cartController = Get.find(); - cartController.addToCart(variant, qty: qty, locationId: locationId.toString()); - Navigator.of(Get.context!).pop(); - Fluttertoast.showToast( - msg: "Added to cart", - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.green, - textColor: Colors.white, - fontSize: 14.0, - ); - } - : null, + Text( + "$qty", + style: TextStyle( + fontSize: isTablet + ? 18 + : 16, + fontWeight: + FontWeight.bold), + ), + IconButton( + icon: const Icon( + Icons + .add, + color: Colors + .green), + onPressed: variantInStock + ? () => variantController.increaseQuantity(selectedId) + : null, + ), + ], + ), + SizedBox( + width: scaleButtonWidth( + MediaQuery.of(context) + .size + .width), + height: scaleButtonHeight( + MediaQuery.of(context) + .size + .height), child: - Text( + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: variantInStock ? ColorConstants.primaryColor : Colors.grey.shade400, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: variantInStock - ? "Add to Cart" - : "Unavailable", - style: TextStyle( - // ✅ Larger button text on tablet - fontSize: isTablet ? 18 : 16, - fontWeight: FontWeight.bold, - color: variantInStock ? Colors.white : Colors.grey.shade700, + ? () { + final cartController = Get.find(); + cartController.addToCart(variant, qty: qty, locationId: locationId.toString()); + print(variant); + Navigator.of(Get.context!).pop(); + Fluttertoast.showToast( + msg: "Added to cart", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: Colors.green, + textColor: Colors.white, + fontSize: 14.0, + ); + } + : null, + child: + Text( + variantInStock + ? "Add to Cart" + : "Unavailable", + style: TextStyle( + // ✅ Larger button text on tablet + fontSize: isTablet ? 18 : 16, + fontWeight: FontWeight.bold, + color: variantInStock ? Colors.white : Colors.grey.shade700, + ), ), ), ), - ), - ], - ); - }), - const SizedBox( - height: 20), - ], + ], + ); + }), + const SizedBox( + height: 20), + ], + ), ), - ), - isScrollControlled: - true, - ); - } else { - final scaffoldMessenger = - ScaffoldMessenger.of( - context); - scaffoldMessenger - .clearSnackBars(); - scaffoldMessenger - .showSnackBar( - SnackBar( - content: const Text( - "This item is currently unavailable", - style: TextStyle( - fontWeight: - FontWeight - .w500, - color: Colors - .white), + isScrollControlled: + true, + ); + } else { + final scaffoldMessenger = + ScaffoldMessenger.of( + context); + scaffoldMessenger + .clearSnackBars(); + scaffoldMessenger + .showSnackBar( + SnackBar( + content: const Text( + "This item is currently unavailable", + style: TextStyle( + fontWeight: + FontWeight + .w500, + color: Colors + .white), + ), + backgroundColor: + Colors.redAccent + .shade700, + behavior: + SnackBarBehavior + .floating, + margin: + const EdgeInsets + .all(12), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius + .circular( + 12)), + duration: + const Duration( + seconds: 4), + action: SnackBarAction( + label: "OK", + textColor: + Colors.white, + onPressed: () { + scaffoldMessenger + .hideCurrentSnackBar(); + }, + ), ), - backgroundColor: - Colors.redAccent - .shade700, - behavior: - SnackBarBehavior - .floating, - margin: - const EdgeInsets - .all(12), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius - .circular( - 12)), - duration: - const Duration( - seconds: 4), - action: SnackBarAction( - label: "OK", - textColor: - Colors.white, - onPressed: () { - scaffoldMessenger - .hideCurrentSnackBar(); - }, - ), - ), - ); - - Future.delayed( - const Duration( - seconds: 2), - () { - if (scaffoldMessenger - .mounted) { - scaffoldMessenger - .hideCurrentSnackBar(); - } - }); - } - }, - child: CircleAvatar( - // ✅ Slightly larger add button on tablet - radius: isTablet ? 20 : 16, - backgroundColor: inStock - ? Colors.pinkAccent - .withOpacity(0.15) - : Colors.black12, - child: Icon( - Icons.add, - // ✅ Larger icon on tablet - size: isTablet ? 22 : 18, - color: inStock + ); + + Future.delayed( + const Duration( + seconds: 2), + () { + if (scaffoldMessenger + .mounted) { + scaffoldMessenger + .hideCurrentSnackBar(); + } + }); + } + }, + child: CircleAvatar( + // ✅ Slightly larger add button on tablet + radius: isTablet ? 20 : 16, + backgroundColor: inStock ? Colors.pinkAccent - : Colors.grey, + .withOpacity(0.15) + : Colors.black12, + child: Icon( + Icons.add, + // ✅ Larger icon on tablet + size: isTablet ? 22 : 18, + color: inStock + ? Colors.pinkAccent + : Colors.grey, + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), - ), - ); - }, - ); - }); - }, + ); + }, + ); + }); + }, + ), ), - ), - ], - ); - }), - ], + ], + ); + }), + ], + ), ), ), ); diff --git a/lib/view/qr_scaner/qr_scaner.dart b/lib/view/qr_scaner/qr_scaner.dart index e04d19b..5a215df 100644 --- a/lib/view/qr_scaner/qr_scaner.dart +++ b/lib/view/qr_scaner/qr_scaner.dart @@ -7,7 +7,7 @@ import 'package:lottie/lottie.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import '../../controllers/tenant/create_tenant.dart'; -import '../../controllers/tenant_controller /tenant_list.dart'; +import '../../controllers/tenant_list/tenant_controller.dart'; import '../home_view.dart'; class QrScannerPage extends StatefulWidget { diff --git a/lib/widgets/fav_button.dart b/lib/widgets/fav_button.dart new file mode 100644 index 0000000..08ee0e0 --- /dev/null +++ b/lib/widgets/fav_button.dart @@ -0,0 +1,118 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class FavButton extends StatefulWidget { + final bool initialValue; + final ValueChanged? onChanged; + final double size; + + const FavButton({ + super.key, + this.initialValue = false, + this.onChanged, + this.size = 40, + }); + + @override + State createState() => _FavButtonState(); +} + +class _FavButtonState extends State + with SingleTickerProviderStateMixin { + late bool _isFav; + late AnimationController _ctrl; + late Animation _scale; + late Animation _particleProgress; + + @override + void initState() { + super.initState(); + _isFav = widget.initialValue; + _ctrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 450), + ); + _scale = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 1.4) + .chain(CurveTween(curve: Curves.easeOut)), + weight: 40, + ), + TweenSequenceItem( + tween: Tween(begin: 1.4, end: 1.0) + .chain(CurveTween(curve: Curves.elasticOut)), + weight: 60, + ), + ]).animate(_ctrl); + _particleProgress = + CurvedAnimation(parent: _ctrl, curve: Curves.easeOut); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + void _toggle() { + setState(() => _isFav = !_isFav); + widget.onChanged?.call(_isFav); + if (_isFav) _ctrl.forward(from: 0); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _toggle, + child: AnimatedBuilder( + animation: _ctrl, + builder: (context, child) => CustomPaint( + painter: _isFav ? _BurstPainter(_particleProgress.value) : null, + child: Transform.scale( + scale: _isFav ? _scale.value : 1.0, + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + color: _isFav + ? Colors.red.withOpacity(0.1) + : Colors.transparent, + shape: BoxShape.circle, + ), + child: Icon( + _isFav + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + size: widget.size * 0.5, + color: _isFav ? Colors.redAccent : Colors.grey, + ), + ), + ), + ), + ), + ); + } +} + +class _BurstPainter extends CustomPainter { + final double progress; + _BurstPainter(this.progress); + + @override + void paint(Canvas canvas, Size size) { + if (progress == 0) return; + final center = Offset(size.width / 2, size.height / 2); + final paint = Paint() + ..color = Colors.redAccent.withOpacity(1 - progress); + const count = 7; + for (int i = 0; i < count; i++) { + final angle = (i / count) * 2 * pi; + final dist = progress * 24; + final pos = center + Offset(dist * cos(angle), dist * sin(angle)); + canvas.drawCircle(pos, 3 * (1 - progress * 0.5), paint); + } + } + + @override + bool shouldRepaint(_BurstPainter old) => old.progress != progress; +} \ No newline at end of file diff --git a/lib/widgets/grocery_card.dart b/lib/widgets/grocery_card.dart new file mode 100644 index 0000000..b797ef9 --- /dev/null +++ b/lib/widgets/grocery_card.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +import '../modules/search/grocery_item.dart'; + +class GroceryCard extends StatelessWidget { + final GroceryItem item; + final VoidCallback onTap; + + const GroceryCard({ + super.key, + required this.item, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Card( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(item.emoji, style: const TextStyle(fontSize: 30)), + Text(item.name), + Text(item.quantity), + Text(item.inCart ? "Added" : "Add"), + ], + ), + ), + ); + } +} + +class StoreCard extends StatelessWidget { + final dynamic storeData; + + const StoreCard({ + super.key, + required this.storeData, + }); + + @override + Widget build(BuildContext context) { + final store = storeData["store"]; + + return Container( + margin: const EdgeInsets.only( + bottom: 15, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + store["name"], + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 8), + + Text(store["address"]), + + const SizedBox(height: 12), + + Row( + children: [ + Expanded( + child: Text( + "₹${store["total_discounted_price"]}", + style: const TextStyle( + fontSize: 24, + color: Colors.green, + fontWeight: + FontWeight.bold, + ), + ), + ), + + ElevatedButton( + onPressed: () {}, + child: const Text( + "Select Store", + ), + ), + ], + ), + + Text( + "Save ₹${store["total_savings"]}", + style: const TextStyle( + color: Colors.green, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/search_widgets.dart b/lib/widgets/search_widgets.dart new file mode 100644 index 0000000..a7d6b77 --- /dev/null +++ b/lib/widgets/search_widgets.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import '../modules/search/grocery_item.dart'; + +// ─── Grocery Card ───────────────────────────────────────────────────────────── + +class GroceryCard extends StatelessWidget { + final GroceryItem item; + final VoidCallback onToggle; + + const GroceryCard({super.key, required this.item, required this.onToggle}); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: item.inCart ? const Color(0xFFF8FDF9) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: item.inCart ? const Color(0xFF1A6B3C) : const Color(0xFFE2E8E0), + width: 1.5, + ), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 14, 12, 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(item.emoji, style: const TextStyle(fontSize: 36)), + const SizedBox(height: 8), + Text( + item.name, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1F1C), + height: 1.3, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFFFF3E8), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + item.quantity, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFFF97316), + ), + ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: onToggle, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: item.inCart + ? const Color(0xFF1A6B3C) + : const Color(0xFFE8F5EE), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + item.inCart ? '✓ Added' : '+ Add to cart', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: item.inCart ? Colors.white : const Color(0xFF1A6B3C), + ), + ), + ), + ), + ], + ), + ), + if (item.inCart) + Positioned( + top: 8, + right: 8, + child: Container( + width: 20, + height: 20, + decoration: const BoxDecoration( + color: Color(0xFF1A6B3C), + shape: BoxShape.circle, + ), + child: const Icon(Icons.check_rounded, color: Colors.white, size: 12), + ), + ), + ], + ), + ); + } +} + +// ─── Skeleton Card ──────────────────────────────────────────────────────────── + +class SkeletonCard extends StatefulWidget { + const SkeletonCard({super.key}); + + @override + State createState() => _SkeletonCardState(); +} + +class _SkeletonCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _ctrl; + late Animation _anim; + + @override + void initState() { + super.initState(); + _ctrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 900)) + ..repeat(reverse: true); + _anim = Tween(begin: 0.4, end: 1.0).animate(_ctrl); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _anim, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8E0), width: 1.5), + ), + padding: const EdgeInsets.fromLTRB(12, 14, 12, 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + color: Color(0xFFE8EDE9), + shape: BoxShape.circle, + ), + ), + const SizedBox(height: 10), + _skLine(0.8), + const SizedBox(height: 6), + _skLine(0.6), + const SizedBox(height: 10), + Container( + height: 32, + decoration: BoxDecoration( + color: const Color(0xFFE8EDE9), + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + ), + ); + } + + Widget _skLine(double widthFraction) { + return FractionallySizedBox( + widthFactor: widthFraction, + child: Container( + height: 12, + decoration: BoxDecoration( + color: const Color(0xFFE8EDE9), + borderRadius: BorderRadius.circular(6), + ), + ), + ); + } +} + +// ─── Chip ───────────────────────────────────────────────────────────────────── + +class SearchChip extends StatelessWidget { + final String label; + final VoidCallback onTap; + final bool muted; + + const SearchChip({ + super.key, + required this.label, + required this.onTap, + this.muted = false, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: muted ? const Color(0xFFEDF2F0) : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: muted ? Colors.transparent : const Color(0xFFE2E8E0), + width: 1.5, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: muted ? 12 : 13, + color: muted ? const Color(0xFF6B7A72) : const Color(0xFF1A1F1C), + ), + ), + ), + ); + } +} + +// ─── Cart Bottom Sheet ──────────────────────────────────────────────────────── + +class CartSheet extends StatelessWidget { + final List items; + final void Function(GroceryItem) onRemove; + + const CartSheet({super.key, required this.items, required this.onRemove}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFE2E8E0), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + const Text( + 'Your Cart', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFF1A1F1C)), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFEDF2F0), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${items.length} items', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF6B7A72)), + ), + ), + ], + ), + const SizedBox(height: 16), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.45, + ), + child: ListView.separated( + shrinkWrap: true, + itemCount: items.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (_, i) { + final item = items[i]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Text(item.emoji, style: const TextStyle(fontSize: 28)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1F1C))), + const SizedBox(height: 2), + Text(item.quantity, + style: const TextStyle( + fontSize: 12, + color: Color(0xFFF97316), + fontWeight: FontWeight.w500)), + ], + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline_rounded, + color: Color(0xFFE24B4A), size: 20), + onPressed: () => onRemove(item), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('🛒 Order placed successfully!'), + behavior: SnackBarBehavior.floating, + backgroundColor: Color(0xFF1A6B3C), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A6B3C), + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), + textStyle: + const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + child: const Text('Place Order'), + ), + ), + ], + ), + ); + } +} + +// ─── Section Label ──────────────────────────────────────────────────────────── + +class SectionLabel extends StatelessWidget { + final String label; + + const SectionLabel(this.label, {super.key}); + + @override + Widget build(BuildContext context) { + return Text( + label.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF6B7A72), + letterSpacing: 0.8, + ), + ); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7299b5c..e12c657 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 786ff5c..4453582 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + gtk url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b9e53ee..a7f659c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,9 +5,11 @@ import FlutterMacOS import Foundation +import app_links import connectivity_plus import device_info_plus import file_selector_macos +import firebase_analytics import firebase_auth import firebase_core import firebase_messaging @@ -20,14 +22,17 @@ import package_info_plus import path_provider_foundation import share_plus import shared_preferences_foundation +import speech_to_text import sqflite_darwin import url_launcher_macos import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) @@ -40,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index a086d13..39943f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -89,6 +121,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + camera: + dependency: "direct main" + description: + name: camera + sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777" + url: "https://pub.dev" + source: hosted + version: "0.12.0+1" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "50c3fd81228826635af4875330febb193c74a2e12a4d19c1b95e2a67f0017bb3" + url: "https://pub.dev" + source: hosted + version: "0.7.3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + url: "https://pub.dev" + source: hosted + version: "0.10.1" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "1245a480a113437f8d46d19c0fb90cea9db921436d9cf2ba5fb11854a1312693" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" + card_animation_hover: + dependency: "direct main" + description: + name: card_animation_hover + sha256: fab2ff2268dcb2ed9e9cd1803ac752911a1f930892fe36a9363b3dc9c6214c90 + url: "https://pub.dev" + source: hosted + version: "0.0.2" carousel_slider: dependency: "direct main" description: @@ -101,10 +205,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -321,6 +425,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+4" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: "4f85b161772e1d54a66893ef131c0a44bd9e552efa78b33d5f4f60d2caa5c8a3" + url: "https://pub.dev" + source: hosted + version: "11.6.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: a44b6d1155ed5cae7641e3de7163111cfd9f6f6c954ca916dc6a3bdfa86bf845 + url: "https://pub.dev" + source: hosted + version: "4.4.3" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: c7d1ed1f86ae64215757518af5576ff88341c8ce5741988c05cc3b2e07b0b273 + url: "https://pub.dev" + source: hosted + version: "0.5.10+16" firebase_auth: dependency: "direct main" description: @@ -422,14 +550,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_contacts: - dependency: "direct main" - description: - name: flutter_contacts - sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae" - url: "https://pub.dev" - source: hosted - version: "1.1.9+2" flutter_floating_bottom_bar: dependency: "direct main" description: @@ -688,6 +808,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.14+2" + google_mlkit_commons: + dependency: transitive + description: + name: google_mlkit_commons + sha256: "9990a65f407a3ef6bae646bf10143faa93fec126683771465bc6c0b43fb0e6e9" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + google_mlkit_object_detection: + dependency: "direct main" + description: + name: google_mlkit_object_detection + sha256: c516ed16c011a47c3abc8f087c6183fa1980b032278a4a1b45872e98d6f4180e + url: "https://pub.dev" + source: hosted + version: "0.13.1" + google_mlkit_text_recognition: + dependency: "direct main" + description: + name: google_mlkit_text_recognition + sha256: "179349417066fa2c275d7a6ed6cbceeb7fa265d73aacdb2d732f1a2991face0a" + url: "https://pub.dev" + source: hosted + version: "0.13.1" google_places_flutter: dependency: "direct main" description: @@ -696,6 +840,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + home_widget: + dependency: "direct main" + description: + name: home_widget + sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3" + url: "https://pub.dev" + source: hosted + version: "0.6.0" html: dependency: transitive description: @@ -817,13 +977,13 @@ packages: source: hosted version: "4.2.5" intl: - dependency: transitive + dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" intro_slider: dependency: "direct main" description: @@ -916,26 +1076,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -992,6 +1152,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" otp_timer_button: dependency: "direct main" description: @@ -1152,6 +1320,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.6" + play_install_referrer: + dependency: "direct main" + description: + name: play_install_referrer + sha256: "7dd808236d35d15199d5e7a5b55488e4343c1b67c4694902d6b95196b22b19cd" + url: "https://pub.dev" + source: hosted + version: "0.5.0" plugin_platform_interface: dependency: transitive description: @@ -1301,6 +1477,14 @@ packages: description: flutter source: sdk version: "0.0.0" + sleek_circular_slider: + dependency: "direct main" + description: + name: sleek_circular_slider + sha256: "89c7d0d37befc9818a2b53ef6ff126c43b70485dabb189b734d46106db190931" + url: "https://pub.dev" + source: hosted + version: "2.1.0" sliver_tools: dependency: "direct main" description: @@ -1333,6 +1517,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + speech_to_text: + dependency: "direct main" + description: + name: speech_to_text + sha256: "75587f7400f485fdf166beacd471549d98fe5d58e634f708916bb65dec05d6a4" + url: "https://pub.dev" + source: hosted + version: "7.4.0" + speech_to_text_platform_interface: + dependency: transitive + description: + name: speech_to_text_platform_interface + sha256: a7e16e02853853ed7534ac2bde9a1c4f39c8879970a7974ac6ff832d4bdaa4b0 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + speech_to_text_windows: + dependency: transitive + description: + name: speech_to_text_windows + sha256: "2d1d10565b23262386b453b33656299608dc7a66784453735d6c1318f13f44d7" + url: "https://pub.dev" + source: hosted + version: "1.0.1" sprintf: dependency: transitive description: @@ -1433,10 +1641,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.11" timezone: dependency: "direct main" description: @@ -1589,6 +1797,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.23.0" + wheel_chooser: + dependency: "direct main" + description: + name: wheel_chooser + sha256: "3fee36f081f321c58a0b7b4afcdd92599f2ca520b3a1420084774e6b19cca1d8" + url: "https://pub.dev" + source: hosted + version: "1.1.2" win32: dependency: transitive description: @@ -1630,5 +1846,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.1 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 946b497..d43bec6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "Nearle Neighbourhood Living Platform" publish_to: 'none' -version: 1.6.5+258 +version: 1.6.8+261 environment: sdk: ^3.8.1 @@ -27,7 +27,6 @@ dependencies: photo_view: ^0.15.0 readmore: ^3.0.0 animated_text_kit: ^4.3.0 - flutter_contacts: ^1.1.9 share_plus: ^10.0.2 rounded_loading_button_plus: ^3.0.1 sms_autofill: ^2.4.1 @@ -74,6 +73,26 @@ dependencies: flutter_speed_dial: ^7.0.0 sliver_tools: ^0.2.12 razorpay_flutter: ^1.3.6 + play_install_referrer: ^0.5.0 + app_links: ^6.3.3 + firebase_analytics: ^11.6.0 + speech_to_text: ^7.0.0 + cached_network_image: ^3.3.1 + wheel_chooser: ^1.1.2 + card_animation_hover: ^0.0.1 + intl: ^0.20.3 + sleek_circular_slider: ^2.1.0 + camera: ^0.12.0+1 + google_mlkit_object_detection: ^0.13.0 + google_mlkit_text_recognition: ^0.13.0 + home_widget: ^0.6.0 + + + + + + + @@ -110,6 +129,7 @@ flutter: - assets/lotties/ + fonts: - family: Proxima Nova fonts: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f824765..7ef518c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,9 +14,12 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( @@ -30,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + SpeechToTextWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SpeechToTextWindows")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index adfbf8c..df83eda 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus file_selector_windows firebase_auth @@ -10,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows permission_handler_windows share_plus + speech_to_text_windows url_launcher_windows )