From e1cdf44bda5aec5b065e266b9669d47fdc35a754 Mon Sep 17 00:00:00 2001 From: Mohannad Otaibi Date: Mon, 13 Jan 2025 02:03:06 +0300 Subject: [PATCH] fix: cursor ai touches --- astro.config.mjs | 7 +- public/assets/images/icons/cursor.sh.png | Bin 0 -> 797 bytes public/assets/images/icons/ente.io.png | Bin 0 -> 677 bytes public/assets/images/icons/frappe.io.png | Bin 0 -> 229 bytes public/assets/images/icons/hoppscotch.io.png | Bin 0 -> 991 bytes public/assets/images/icons/linear.app.png | Bin 0 -> 1707 bytes public/assets/images/icons/slack.com.png | Bin 0 -> 1214 bytes public/assets/images/icons/www.cursor.com.png | Bin 0 -> 797 bytes .../assets/images/icons/www.heidisql.com.png | Bin 0 -> 1379 bytes .../assets/images/icons/www.parrotsec.org.png | Bin 0 -> 2422 bytes .../assets/images/icons/www.rapidee.com.png | Bin 0 -> 874 bytes .../images/icons/www.screamingfrog.co.uk.png | Bin 0 -> 3651 bytes public/assets/images/icons/www.thiqah.sa.png | Bin 0 -> 1740 bytes src/assets/data/db.json | 334 +- src/assets/data/general.json | 87 + src/assets/data/linked/achievements.json | 58 + src/assets/data/linked/applications.json | 218 + src/assets/data/linked/bookmarks.json | 162 + src/assets/data/linked/games.json | 46 + src/assets/data/linked/languages.json | 146 + src/assets/data/linked/operating_systems.json | 36 + src/assets/data/linked/organizations.json | 78 + src/assets/data/linked/standards.json | 42 + src/assets/data/locations.json | 12017 ++++++++++++++++ src/assets/data/quotes.json | 26 + src/components/Footer/Contact.astro | 160 +- src/components/Footer/Footer.astro | 2 +- src/components/Footer/InputField.astro | 48 +- src/components/Header/Header.astro | 16 +- src/components/Header/Tag.astro | 3 +- src/components/Header/Tags.astro | 8 +- src/components/Quote.astro | 50 +- src/components/Sections/GeneralSection.astro | 2 +- src/components/Sections/Link.astro | 5 +- src/components/Sections/MapSection.astro | 129 +- src/components/Sections/QuotesSection.astro | 18 +- src/layouts/Layout.astro | 3 +- src/pages/index.astro | 40 +- src/utilities/getIcon.ts | 206 +- 39 files changed, 13417 insertions(+), 530 deletions(-) create mode 100644 public/assets/images/icons/cursor.sh.png create mode 100644 public/assets/images/icons/ente.io.png create mode 100644 public/assets/images/icons/frappe.io.png create mode 100644 public/assets/images/icons/hoppscotch.io.png create mode 100644 public/assets/images/icons/linear.app.png create mode 100644 public/assets/images/icons/slack.com.png create mode 100644 public/assets/images/icons/www.cursor.com.png create mode 100644 public/assets/images/icons/www.heidisql.com.png create mode 100644 public/assets/images/icons/www.parrotsec.org.png create mode 100644 public/assets/images/icons/www.rapidee.com.png create mode 100644 public/assets/images/icons/www.screamingfrog.co.uk.png create mode 100644 public/assets/images/icons/www.thiqah.sa.png create mode 100644 src/assets/data/general.json create mode 100644 src/assets/data/linked/achievements.json create mode 100644 src/assets/data/linked/applications.json create mode 100644 src/assets/data/linked/bookmarks.json create mode 100644 src/assets/data/linked/games.json create mode 100644 src/assets/data/linked/languages.json create mode 100644 src/assets/data/linked/operating_systems.json create mode 100644 src/assets/data/linked/organizations.json create mode 100644 src/assets/data/linked/standards.json create mode 100644 src/assets/data/locations.json create mode 100644 src/assets/data/quotes.json diff --git a/astro.config.mjs b/astro.config.mjs index 4316e98..44a7c60 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -10,5 +10,10 @@ export default defineConfig({ site: "https://mohannadotaibi.com", integrations: [tailwind(), image({ serviceEntryPoint: "@astrojs/image/sharp" - }), robotsTxt(), sitemap()] + }), robotsTxt(), sitemap()], + vite: { + optimizeDeps: { + exclude: ['leaflet'] + } + } }); \ No newline at end of file diff --git a/public/assets/images/icons/cursor.sh.png b/public/assets/images/icons/cursor.sh.png new file mode 100644 index 0000000000000000000000000000000000000000..e406562350fc115f6715f549d35d2c1078a700d3 GIT binary patch literal 797 zcmV+&1LFLNP)mkX|1~u=jEsz!n3zUJ zMn6A4v9Ymoa&phl&skYn;NajL9Ua!(CIA2c019+cPE!Dg3wtp}obt8RD`4JT0007X zNklZ>5{7;5QVKIZV)aYBKrO>dTB(FRgH71GCxMo=jldSs>-Q;H(lSiZTrng zU+Vj=dDm4!pDt%hsrv->*Dbq2yu8O}Q|HwlLe%%|puH8J-~8XN&G7mm|EhNF!|nb# zb&m=FeNF>NdK#g0eL!5`rF2Je2r1(L7(@Y4SMS`VMRJ4yfbo#8<6SkGD++;s1>gba z5KX3sm>K&90Hy2bsy^{wDy{tpNVlR-0HF8;Al-_-ir!=8+yOu;{g^q|2r2^SpXH3kuy6PmplMtLC3P4%>HPG06kJ_%KK+R zm^cCG5Hei{*vUlk82}I{WuJi{#RY(_BWi_tyzU^ymjFZ13jK6q0w{3};J3m&-S{EE zRRAEkE8bKvV{r$dZ^ff6-jtw+F#06R0HGwYkB2`Kjlbz)34`Q}*zzHV6n6juOFgx) zExrDYG%>5>0m@7*jBFJUGT`d~q3q1WTm_t720-z4N@H7oJA_vNYw-?XJ2)bVFEw#b z?-H1=kRi_0`Fr5o*94}5b)jRY--1NGwgfuBI?c=8f*)RG8-OS507!3f_Es?4!KiqvF1yLHtzhsG=;B4@wkE<ZfeC0LkL5-o>+m zgc)hfr8la}TV1_lnM!up>Yi-2aXvk$`-|=V;{(U@BhTYQ*YjiF^MmK}qwn*>_u7hI bj{pAw;AOwDp&wg700000NkvXXu0mjfX>4c8 literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/ente.io.png b/public/assets/images/icons/ente.io.png new file mode 100644 index 0000000000000000000000000000000000000000..7c50971f8a67539f2160d50df61b3fd674710fce GIT binary patch literal 677 zcmV;W0$TlvP)LQM(E_3alJ!2vj#4HO!Xuahl-0!me|l31j0S}Nn<+E^;N{bevC2q}$kq!>{Ti0{ z8h1zM#s^qGsR0(#az#^BB{t6u?F8UT(74^yVmTIS=Uf0eU^D_?bwPfj-54N1W)mdG zLb*y*${PSq;X^X8T<=X9V;D4+bNcDy=2}zlwzjOVFMjL*$m5)X zXYJH!ZnURDXb*fY-;q*9XtnN8i_nZ<6Q7e^UMf>%~l=JkKFN00000 LNkvXXu0mjfC{Qj> literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/frappe.io.png b/public/assets/images/icons/frappe.io.png new file mode 100644 index 0000000000000000000000000000000000000000..1fada0bb13f9120d26c6f47b906b32349adf6793 GIT binary patch literal 229 zcmV^P)g7l{RMt<0AZ+i+dbJzNBng_A{iiqZu@jt^{ErRrhAiW_-ZwS(x fVS)evfRVfbB9R=-zW%h600000NkvXXu0mjfEY@Gt literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/hoppscotch.io.png b/public/assets/images/icons/hoppscotch.io.png new file mode 100644 index 0000000000000000000000000000000000000000..3ca74b0d757b74c751ec7e1a7277dd1b44ec987b GIT binary patch literal 991 zcmV<510ei~P)P)t-sM{rCB zDIf?P7YHL92QVcECLRYhC8SJkQSlr7%o~I0%$~AikC=UYJ~tT>|Et zozO|Jc0qV4KEG7Z@-1u7mxQmHzcYjhLpoM5`B(Bg0Ziz892ldMN~wlWoq%%*cICk6 zflzSm!c$EW27(J*b|^5N3!(@s+vz3%_pX9I_%q$Bux9|6Oi*6s zm{QCJ%+XBySbPh>r&3)JB|W-r+X8-VE<)#}EplLvTs=P*+t2u-XSa77On%msjI02>Mc0jUCvKM;Kl zGeDIFL;`?LNh=;Efe4^=8}da7yplw~HN<7Wsel|!MQ|$wU#HugDIg7wjR2aTqDE)ebvVaQL_f?M%K*DUyExsL~ITK!N)+?Gvsn%e8{mP-%oP zsE+9b@X|-TD(oVn&8fj+%^v`Fsh}F+E$(iamI2@6FF9-SvXpQi0KU^u8;X|0^N8^U zvrHSM^EU~&i4wX(#xV8rsf@ffJ~+kq$y~*hTn>6KFYixG_8?^_A;JtCC2ufZAWZ1H za;l*+h8w+>KU6U|VLFD)Cll5Z&fJ`Cn4uE^fGhS@?r0pIWux@b7AJ_{GLws z{;$2)|GWO{ve)__mb9cLEon*rzY6cfO9mfHM&CvE2&sH&O4BM5ihB^Nj-FbXkTKDOrw6X+9c)3yG05=2FOO9+Va3`?t+cRp<1n1G|&FQq#Ic zbFah=l3Vd%ti@_vj_%U(0?y(Myn-WmvHbmSN)CZam2YmJyK2wblO|d`q9UleNwNiZ zVm(&1@xFx9_&uJ&!LHy>qe&VMs{Y=&srsy+GBiBdnU}BQ{BaxbWqbx}X8Zh7ui`*p ze>FLpg@SXAzFC5pRq3Igxpd28I)-#s|*Xj1My|Ikcw;`7}nT zxD7-2_>61Px)Pp4Uy2TAS#fcukualDLwgg6kmM=28#{5$jA}4*y@qe+$X?8q`dO3^6oM?%*2D+7V96nz6bES&Rj8NM5%uZC3k zvU6{0Qe>*y2Y|$tic!A1ne98E=m#Wp`!mi=gA+!x36@}iw!#Lh<`FYYxd?(9o zOo_2J3bxc?hNclssaU(?4h&;m8}FP?J%=wU8mOhkQqu_7xvy|OB{}X4tPA7IF`h-Q z#S;miz_wU=HSl=zuFw>~__{s>8}L|SUtpcZ3t?Lp-3;MUbK{oAfRL#uoV*6NCx#a> zgf+NlEEiho;LQOfQgk^s)Lgw|kwf@=rRv6znoo;O0ffZJmEJ@@{#tWuF+;c>n<+k_ z8>yuT=y{VAJdOKx4us8u9!czHvf^BQqxxe&KxR2^#@3WPs`DV+kaEi!8Po?*?B@31s`AQbI}pNqv7(VdvsqyJ z$kAyKE|V;8G!rxgkfKxAg_AXxI|)L!$+L&IRYL$FNLiOB@HL$ap`q#m=iu z@QS42u0Ue|#advqWVhhzkarY>grfqfftRgph`f%ik{{Ah5dNt+VQ#qd*~;heOLz%i zko-XBK=?xl{v5F5-ueWZcY(|q{HnZiR|i3ODn;j-ZHu+EA%b6;bg&a3yoQ6V>WU8hB;I_{V2nEe^iZqo?rfxJW1_*!012`5G*w$RGHP&%$^X6@K06pWyCReQFN0U zyI+LhLCN1*J$y-tJSRr0aYicggW7eTHX~qM6OllyRJ;(Qqqx!3{J{B$gj)i?5uAyU z5R6b{h|R=#W}7HEHFIPQ9>m?IM#Ef2!ao#=m3(Zkq^ z$4&iqC_bfI+*z7-47VLGC|07M#?!N@^K{J(MhuqG5D=0|3T{!r*rXV&4hXhmBt`PB zx{+`|(aNVo(IiXPYG`*7@|tw{V*b8D0pMCi=J#9U_iG^Zl_gH% zWy$vhCqgWqkwQ&HBYlf8U4FWThSTKVL#C;}HyGSgP>5qf-eq+txSh*5A4{iqbn-t& zTG2R!5NiHL6UZ--avphLL94fn% literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/slack.com.png b/public/assets/images/icons/slack.com.png new file mode 100644 index 0000000000000000000000000000000000000000..2cff8e0e5f237636a4355e23a120e249cfc62220 GIT binary patch literal 1214 zcmV;v1VQ_WP)2uviTkM<#TE68tAA2QI& zNE0hcW#yI?jLDo^ZZe8$4PCA<3!P45+u1qKefQyP?zo-fIrD%QJRhFxez~qc{}*l{ z11vn&TLNqbdlcl92yZ*{jx;UFH-OT*tzt-}sck^GP2dp7yY1oXbLn~q(j_cv>J?Qx z2+WQ%Hp7fZ2-&?rCx9YPqq;X927`NmQXrhJze|^($eI!H5##=#|0O_aZB;I+pN_4x z$L_1GzB6kBVmc#5X!g?Mk&vR3fZSxgRs03n8xZ#m5K9(|`hyNh-NP_1YXip@7yN|C zTj}y{6lo73K+BTCZDozU-y)P5LSxh7!uDa#Q-_bAkyzQ`s@n&|e;ARD_T_K10|d&NE|&~M2loT_r|n?i z8!T+RbU1HP=&AZyc~O)2#ZQO1wOe27H@_MqgqhB%-Eu@sUUa5fm67WLxaWcX z%(UgQb8c1hqJoBDCO6sPf}p67W~a z+6n5sIOlnhsU06huZhoE7n*WMNmS~ql3qNknJ>K_6H+Yzz!P+0xV3NiU(QThD`u37 z>1PI$uVxAxrk&Zh6X4(JFXi|9k==%^5|LBl?AfVZ``Qv4_k3Nt($wBJY%=K}(h&>= zSHyhna+pVD{O& z*5|}xej#S#6_582neGI>8zYXGc&>RkEmZ^O4@k(|c@g>fMhuAA_YUX1&d_ixc>emzsNZkDO87PH9#CkLi0@W+x9liC5wm)x*0fizn+mXl_{@{h0xzA0iX#Xl*ecGuwJ(64!vyhp6V28uO ztPPk*%8A1dcqVA7fvl)U|9&8A10W}p)6zMmmkX|1~u=jEsz!n3zUJ zMn6A4v9Ymoa&phl&skYn;NajL9Ua!(CIA2c019+cPE!Dg3wtp}obt8RD`4JT0007X zNklZ>5{7;5QVKIZV)aYBKrO>dTB(FRgH71GCxMo=jldSs>-Q;H(lSiZTrng zU+Vj=dDm4!pDt%hsrv->*Dbq2yu8O}Q|HwlLe%%|puH8J-~8XN&G7mm|EhNF!|nb# zb&m=FeNF>NdK#g0eL!5`rF2Je2r1(L7(@Y4SMS`VMRJ4yfbo#8<6SkGD++;s1>gba z5KX3sm>K&90Hy2bsy^{wDy{tpNVlR-0HF8;Al-_-ir!=8+yOu;{g^q|2r2^SpXH3kuy6PmplMtLC3P4%>HPG06kJ_%KK+R zm^cCG5Hei{*vUlk82}I{WuJi{#RY(_BWi_tyzU^ymjFZ13jK6q0w{3};J3m&-S{EE zRRAEkE8bKvV{r$dZ^ff6-jtw+F#06R0HGwYkB2`Kjlbz)34`Q}*zzHV6n6juOFgx) zExrDYG%>5>0m@7*jBFJUGT`d~q3q1WTm_t720-z4N@H7oJA_vNYw-?XJ2)bVFEw#b z?-H1=kRi_0`Fr5o*94}5b)jRY--1NGwgfuBI?c=8f*)RG8-OS507!3f_Es?4!KiqvF1yLHtzhsG=;B4@wkE<ZfeC0LkL5-o>+m zgc)hfr8la}TV1_lnM!up>Yi-2aXvk$`-|=V;{(U@BhTYQ*YjiF^MmK}qwn*>_u7hI bj{pAw;AOwDp&wg700000NkvXXu0mjfX>4c8 literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/www.heidisql.com.png b/public/assets/images/icons/www.heidisql.com.png new file mode 100644 index 0000000000000000000000000000000000000000..b6fffda4aefb217a475aa5e50f9a4713332890d1 GIT binary patch literal 1379 zcmV-p1)TbcP)dLXnw~nWCBQ@9v|kqs8U0Cv*X7twJ=503l@nbi_U`egQg{0A{5^ zKcxV**Px)fkHW>mK1nx3umDzARV_9q((A%Xxd5EeeS^kcX?0>?yDNW}a7mROTcR>p zwjUL440Zqj01R|ePE!DEpyqPlrc4~5+4zLG{VV_g1Y}7>K~z|U%~$Dq;y4f_6B9#% z4?xBz0$g}c!!$=f;uVc zi3)kI!YlG+HlJQG=hs|Mfez^D!!IdQnP&U_mC%m`DctRaDO2X}_vcUf(#1LqcnKZyDGF$^fU%^TOKZ{wQ z?g(sJKBi>`NKv3!!)vliRyg`VASU~pa&ut;36*jMlQ#R1Gy<|;pv0-Ulh_0j$bix| zVTB3Mbpbdwt$@}RY9a|FE1;?S2*Ongce0e$jRQ*)m~2qg!dFiBJqoNwHv&=^cV zO{DgnEaqGCD!{koQ!9`*9j|wYTD9Fe@eZ8W!iH-*0<^CQ+%MXe@xGOC8|_qbvM~F= zQY5W-AB_Oug#cmOj@J?J92-V`mF+OIv9>}MJ1`@Fix+IiA%WeDu_FOOI8z=Vrcw`C znt(^jN?^+*XUhUMXY2xNLa42f#k(-VumZX+U?WWlaKe`+6~s`f3k)C%D&&Wb-6LRA zh66PTfCI3A{6L+=0AuP*uK?7mylE*)a$2{tg+1gMR^CxoJfR2DWjPOC-ZLR4J?Xu%JB zA1}JDaD@VgZQ2e4-w!%pTv5}HRJE%f?bFpFA#k$5zvT~1Fn|g!L-0#9iOR`dMEtui z->?Xfh(tqgA#P*f<5c4=A5N2xh}qiNpqD=w%}Cs@ zTn{QedTT;@cioChX1)AYVHPM-2W}P29{X?lFvG_zs_rC^P8UDp!(R9_36w^aPOl;y z!z+FL*J1JpI{AQ4tqK62cQnHI`p4Ip*|+eO=C^)$jmBMigD>1`FoyT+;kSSGu?=Ia li44a-{P}7Ue#HIn@Gm@O>kB{i?g0P*002ovPDHLkV1lL5bFcsa literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/www.parrotsec.org.png b/public/assets/images/icons/www.parrotsec.org.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf55e786b83db8e56a41ac9ae97b06b02c3f613 GIT binary patch literal 2422 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 literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/www.rapidee.com.png b/public/assets/images/icons/www.rapidee.com.png new file mode 100644 index 0000000000000000000000000000000000000000..d1468a96154a6cbcd36ee1a012fda531cc649126 GIT binary patch literal 874 zcmV-w1C{)VP)D|(jRWclR?QWi_xPr1!LL$mHRz{g&tDR_u+E{;HjeHXgwv=3L zO3`dFm?N00q&JZ)>$Em^;+#m>NFvYM{du0#dmit(=jlxa4e9&t3tzrp`0@J)Y%h<) z;qVu^i%S5~N@`uxv?U;Udr-K|yQ}W6YU}I_w^!Bf4~RCa5WvvP%+=ZXh0&4Gv1`|& zp99Kv1$Xzq(RU);)A95#zfLi?6d|o95Cj32y^vr<4RyXoMu*Rg4-F3W0jmJgKg+%S zy&reZ%{)kN8c_eKUe#<|uZ1IsE*4E>DrCR0hBjP*s(j zkw?snXqtu~zh9HXcw+--6T?4gyAEn4v%AI8;Y-Mws$y8!ZG7Mv*y1J&RY>#*Mp4~p0 zAA6R$w&2YITY6+_UGoF=PF@wthKOM?PHWvH_2ld~TUzj^!+ zvM8fuQdn5Xk_-qK#ul<qkVMVr#J_r>)S}`AS#d+shY8 zD=B1Y2Ur4@XP8w-6gm3(3C^FnbYbG+#K6Cg1n7}SWV*<8$Gva$9t{)}xVV*y zvzg6+0HwR`qN%=_?&sd-%P%I!hdvnU1J<{NDHN(~I}{GT7zp`;c}p(@kOX_?di2J^ z>C+=)(f@(zKRDa%cE8uNs{|mCytS_D`nEX#1y9ahp16*lEC2ui07*qoM6N<$g3pVs Ao&W#< literal 0 HcmV?d00001 diff --git a/public/assets/images/icons/www.screamingfrog.co.uk.png b/public/assets/images/icons/www.screamingfrog.co.uk.png new file mode 100644 index 0000000000000000000000000000000000000000..024c6043256a322f2735094ad9b4e6227de214df GIT binary patch literal 3651 zcmV-J4!rS+P)?m66RqvLfm(FeR78%Ft z(cY0xYrR)dqkxR1mTm*BTx*daNNWQJc|;PDkau$Oex29-W1WzblfC!ZIVYWV=HBnj zo@B4}TfepT@2tJn@Av(!4gbesnr?-%B1L(<-V(2eVzb4R2c!d0A*yo${Xh#)1613r z_O@vnDlkn3kQFHjSP!fl>~`I_XG?A3P_LWvpB0jv73#CA!VR1SDuLa==QgW7EQJx_ zG?o=97uW)91QG!}ZZAVUZif5Z3_4uXZ2$`(`K~8{6S5+`0PM3_?VeB+nnP(MD^iTC zNZSy<#=q1Y?M>YKR4vWt21%F^$=B}B0-&eG8H)O;U@5Q%I3_F7vQQLE*#NR4EdY)K zKL+9fP91d6cxsT}Jk!8XuZxnKQiw~9;L@M_0ciN^0JWd@O?hx$kC&d-QCy=zf83?O z5m}M8%ZfCmS#6n$u&hYyfdfFUe|bV$Bn{OA40O2|=yXx^wPgA_oP7Cl4~uV0qvG+e zxcFH=OV_6x?(5QD`q{Ikj?Z>?Q2n8U+y!wYrbh;QmK%W;W#yR%sw&!tjPzsJ0J0)I z2K)+$4y;R_8O5SoQaM%Wp!w_&DcMnE6vkpd)J<%X#o=*!d;2?GkxRdwehRUo>t6jG z&cVG~>e7b$+yJ;nyqrJMOHN4~Ntr>{ltRE-R-XCWs*3jBV8VuW0a=k`;Kg8a4b=ng zeOv2Xc!^HVj%w|08qEUE0egX)fg6nU2fPeCK54KfD7-$AoRT<_XGW4XJDM4J(U?rC zk8uH58Eo4OY5-Z0HUMv+KU>_u+rSQ+)t(1@0L1#$4*>52_XX<@VWf{ynQ3asD2(Cu z?fJx|sJ{9qfg5dB`=C~P-9=DVq#JHe6)6SbZ0HT3Gh#TWj{hJu7;<)3O4P2 zvi1#xH|o}q$pE&hRcOtC|3Le_e-GGUU|`k2W&Z+`iKW1Sbw9~J^bI+u5@CRF@C;&c z#b&kdM(iG1*rYZKCK*6hq(#6j8cJG$hm8avvLfYVUme@!U$*A)9KQAAJWEDl%$j5G zw10ysI${BM9oQC7wiU4`t05kEAh1wv222w2!#11M-lbEItVqa;v8S~Z*AWk|rKA!#frt+RApu3e`LTk)8L$}0)=<1#W5ox= zFOTpU9vmx+O^RU7(!>dJUG*aj^=hqEx^NNk1E3&y6tOiM!G46n)KKbw6KH@%8jYNY5LgwmA{7JM)#`Gtj$`?~nOaS! zf)1mCHmkh@!BwcC*#9Qb00m?JIGRRryS!t!b$b3ZVq4VQ%Wt?l<6!Q>xI58zR)QgK zit^P)^aL6p9iaY;eqMjz0`JNf>1i1S9Y0=Hq&(o8Is3IH{)CO1fUSMO(`JwE;{!VqroH4XY-j^6w9Ap zZ(D=d-eM4e#fyw@m&*aV8b>&N$bo14wE@`ddw4VOEuCkbgwpjhxZ769eLLrpQxXd> z3-y`y0Ps`bG2i{Qz*Xr5(ay!UrUmUsGZ;$CjV6Cd0=I9^=bE*tBfvw5g|HIgZH;wU zbo!u!T@Tf8=;bCp+SxRw;soRG;UZv8(9w+bJv*9}_X%RvY%#Iy&Wv_n)pJaaD(iiJ zKlc6I1`MoyI`@^7?BI;FssZ}iogClWPVI?4JZ>+mAIUz9U|$ykhkfmq19@JrH~q-2 zR=mC+{?x&4>Q43p9QAXVtwBfPhqCev%n{>UO~JJZ$AC^?KCll+@Dm@Sz3LIK=54_H zGxB2mn4k%>aspIolrtZ7^XZ$SeRP7Eta-6T2yVcQSjevSiQY5N<%$^Wp6J+o@?S0F zFPZC4Mct%1f&irCL~+%MqzPqav&n_v3+_h5DQ6H)*B5@e-$r{!SBa#OrGT@I{U*HE~}~FtE#@c5dOhpeEEZ5NXm@l>eVS))gK_@5iM8U zAK&Y05L;kul7-xb@f2R4s45sVi4YNGCZ{B>3qf;z;=8U4qPJBus*?~TsNvk$l7CJc zFk4Jo#u`SD9`6&}pzfLTV_TMQ$e5MCBmt9IoBN$=W~c_Sof-A$o+BXm&_U~hn^S|u z8;1~!?`InIJ-|+&H8*miCNb9-mlMrfl_+^A6R2E|UK>P{eX#K*Vya%xz9}c(Ukg~(R0X&8_2{{#9 zLgWKnKycS>nh~l2Kv3ClhuME8!bxiKWp$0F&ZZ6F7ytyTYbXn=0w>}%I^(-vhxQg1 z?l?6Os8}_AM0>5MN1uw;!++xQ-+grT3p%o)yc&ku1R6jqovg=e?J${XHZ-JhxB(l0 ztVl(O)qA#%{4!aQ3XCe36)6gRj^YXef7uodxk6cy%o^u#`cr2`J>S(Z`jXKB0+xtq z(~d{>7LOvFrC>x!G*Brk($k2wM{A~<5&XbbMBo$@fis{+LoOAVr=?`>4M{&|axzpG zt`YBd_HV0yaJb*?|L`Ytn#@P!0~!zs8nvq#_#gE1j1!C(Xn;TKG`v}>;>3F$qfAbQ z21Wf}26*R*i*z-NfbcGDQ`Hdy&img48sG@pd;2|ld3NAHTx!I#>wc2o^X=yf`1+&S z6qF{Qet2UX-A$vs^RE{<`RmRjh={LUzeU*C=YKP4c(NiL1FqL-TxPS{kLhWb73o^U zgUkkSG>vfdtv1g6G3ap}fSr%*Dej6%wEVjf;Sf*@EV5bc{bL1_$jq|x%s8}@qjSs3 zGj~;0X#Yr6746Ms<(U--l2VFIjG$;`GP4)O(|m3)SXOA+$^9L9*~M{DDcMoedcqQs zslLr-wKw<|su|!M@O{)*<~hLSs)}~KwSuaO_JOL3_V>!lGe1I{_{dMnii%ozOB%6> z7FsV1;Tln=sk|OizdYO%)!XJI@0xh0CDI&D1{t6a!5-aZv)XF|D^=f6-|^EYH53jY zPG5atbOf>@#Q-HhCg5rMa7gEw?nWH2){GFf z43LgUEzQtqcNth^v)Z+`O{0haZbjt%R-v9>Xh-Dp>Q(B(hN_S4%dI`F({2{<2U(Fc zeFKAVAr{P=5C^4xh6s@Sk%2gn{WpPLkntMuJDvJ`#QEHn(=?W|h~sth5YCQ2!Zoxe zG!5Y6^f#jK;bj4beTS6wj=@c3Jg+c0F3?JJh7BVpMN1GwsMdNghGs-$_(q6dbwnYE z*Ybe!21L3mFg>1!AZCmlz|@4%v%;l_lvpq$!1!5$qlj3)1;OZQqO)OyT@Tf42G;tQ z$0l2xEAGvFVv#lNjDPu&U9FkN_jJ4oj9#@espP_u-jI!8h!eC~?Vrnvv>H&Dm={n( zB(TDF1;tRGi-WewxrO+&2<~{UATl*KdMCi>kejGju}OH`!iO+Nn4(M9%~%|c0Sq%O zHmkiFL0NqoYG;8MaJbmJrH+w}N8@B(W9Ix=E`HX> z$G>bD3zLbAG2`_>b)|y?I~s_HHgoI0uY!BxkkOIzz@)8mj7Y!0OH8TbYDi(Y7_7`odFs!BO<_+2+}0Ycjck) zXkA8RhfgD>dIZ9I9Pv=R@M`)uubljY zwfPirB^uP02xP*IpZuIQ1Rgd^91WH^jbozFa-VpOa%Uf zSfd^$>eZ$?w?y)!^7QAXq&B!y8C2$o7>E;#8jl7m{B?4jhl3S{9Ta8nJlM@ZLa71e zOSN(F7E>y#5m(#|FpBD}len_|@e)6t>*(O+?ue0}kkv4SXkxTcc{mvJMRR#FQvD#1 zHj2!^Gyk_df-8dI0=Sa2nLMiMzi4TGHE!55fr=9XMs&#tiD79O<#w;~xB*bS!RU9b z*z>VO=|mR{80rulut<@}Q61muR`lQ~#e)~m6DwUJem0u$s2)>9cY@muBw@0kPz?Ml zV259R2L~LAf(gXM??pULE(DMXMKG(9q zR#O)kmD!uMrCoKK_SlhN%CKXz4fWpr;rMzRB9&!?Ob!`S!-y17I;mfB(Zd)_*MXZ# zT5zEt^E`v>y@S@sLbxXcz$pH+U#Gfbf-kmWKlX*Qp{vc2ni*DB#Ne6rirXZ z5v9m8{#^$)bn)@rik@~arz0l0r?O{3aaq`ai~@DZTuzTzo1g4AnY-$d{|bwXnnd8$ zuLD%K!^n)vXJk=K`dpNY7C0D@QAvE~@#n0h%qOE4r_*XoQR}l=o>_A9x!ZUO$U!je zdj_~%fI*SodoC9zJYj8qLX+A5A16fVz4bK}H{3=~1+oxR5tvoI_b#YKQa^DtZJHRy zS9%C=sGtV`;ygLY#a=p^&1v-wrtu1e5y2vO#1?{ZB6v+F4afC}X+7es(v41`_i0?L z+0$!qs7g3g0zrfzf=EClpajn2^6Q_D*7tBSe|8Rh{Mj%Ww!;)=lC$~lbD>^72*Bk5A7Mo|TaiUx6| zg#5hFXtv8V?Jg#b7pedOq8&y%V=*G0xcRJBYv<%GXs)PkVlvrv8ZhPVov($*N ztDfJmXC;h_QPen7-AS^t)oHzZR>k*BY}%B>px8z*eI0DOsNs?q0y0X3f+7|f6gyt- zW_z_|V}ok}&PZ zT$Y;2y#4k1>Cx`${0v=7_gOB}sY<$46oM(i6vQx1_`kucFbCPPJCf@k6{@ziv_K1uO+N!nl}{h9WH_*I0bV9 zDJYJZpf3H@55OZ?E+&5@nBJ|5ZH?1xdiDI2<&+o{BPzyn*Ye%z5tka}qw#R(sE!B0 z;vR5GP5V4^12MFpfjEkZk?H!-u)g_4Gg*1BlV*F|olgJw%b&ve1L#_!C1P=*%7uxY z;TD+2A!_M%u(;kyed{a^Z>10~K>-Ixz{8?Pj%%M#p%}I0J58GHAbDR(=IZ$=oIfDW zr%%JusWFTonWb{d%cq|)5BG^?)8lGo=cDnp-4s^0=x^Q|!1X6WL2-$96x$!T-5#g* zVD0=o}aOr iPFPCxJ+t)8|NjBE=!igGI73$e0000
- <!-- Card --> - <div class=" - flex flex-col - rounded-xl - p-4 sm:p-6 lg:p-8 - bg-slate-100 dark:bg-slate-950 - shadow-sm shadow-slate-700 dark:shadow-slate-900 - "> + <div class="grid lg:grid-cols-2 gap-8"> + <!-- Contact Form Card --> + <div class="bg-white dark:bg-slate-800 rounded-2xl p-6 lg:p-8 shadow-lg transform transition-all hover:shadow-xl"> + <form action="https://formsubmit.co/92de50b71f8ded9d6e8c5862f42e305a" method="post" class="space-y-6"> + <!-- Name Fields --> + <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> + <InputField name="firstname" label="First Name" placeholder="John" /> + <InputField name="lastname" label="Last Name" placeholder="Doe" /> + </div> - <form action="https://formsubmit.co/92de50b71f8ded9d6e8c5862f42e305a" method="post"> - <div class="grid gap-4 lg:gap-6"> - <!-- Grid --> - <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-6"> - <InputField name="firstname" label="First Name" placeholder="" /> - <InputField name="lastname" label="Last Name" placeholder="" /> - </div> - <!-- End Grid --> + <!-- Contact Fields --> + <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> + <InputField name="email" label="Email" placeholder="john@example.com" type="email" /> + <InputField name="phone" label="Phone Number" placeholder="+1234567890" type="tel" /> + </div> - <!-- Grid --> - <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 lg:gap-6"> - <InputField name="email" label="Email" placeholder="" type="email" /> - <InputField name="phone" label="Phone Number" placeholder="" type="tel" /> - </div> - <!-- End Grid --> + <!-- Message Field --> + <div class="relative"> + <label for="message" class="block text-sm font-medium text-gray-700 dark:text-white mb-2"> + Message + </label> + <textarea + id="message" + name="message" + rows="4" + class="block w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-600 + text-gray-700 dark:text-gray-300 bg-white dark:bg-slate-900 + focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-20 + transition-colors duration-200" + placeholder="Your message here..." + required + ></textarea> + </div> - <div> - <label for="hs-about-contacts-1" class="block text-sm text-gray-700 font-medium dark:text-white mb-1">Details</label> - <textarea id="hs-about-contacts-1" name="hs-about-contacts-1" rows="4" class="py-3 px-4 block w-full border-gray-200 rounded-md text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:border"></textarea> - </div> + <!-- Submit Button --> + <div class="flex items-center justify-end"> + <button + type="submit" + class="inline-flex items-center px-6 py-3 border border-transparent + text-base font-medium rounded-lg shadow-sm text-white + bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 + focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 + transition-all duration-200 transform hover:scale-105" + > + Send Message + <Icon name="heroicons-outline:paper-airplane" class="ml-2 -mr-1 h-5 w-5" /> + </button> </div> - <!-- End Grid --> - <div class="mt-6 grid"> - <button type="submit" class=" - inline-flex justify-center items-center - gap-x-3 py-3 px-4 - text-center lg:text-base - text-sm - text-white font-medium - rounded-md - bg-indigo-600 - hover:bg-indigo-700 - focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2 focus:ring-offset-white - transition - dark:focus:ring-offset-gray-800 - dark:bg-indigo-900 - dark:hover:bg-indigo-800">Send inquiry</button> + <!-- Response Time Note --> + <p class="text-sm text-center text-gray-500 dark:text-gray-400 mt-4"> + I'll try to respond within 2-5 business days + </p> + </form> + </div> + + <!-- Contact Information Card --> + <div class="bg-gradient-to-br from-indigo-600 to-purple-600 dark:from-indigo-800 dark:to-purple-800 + rounded-2xl p-6 lg:p-8 text-white shadow-lg"> + <div class="space-y-6"> + <h3 class="text-2xl font-bold">Quick Connect</h3> + <p class="text-indigo-100"> + Feel free to reach out through any of these channels: + </p> + + <!-- Contact Methods --> + <div class="space-y-4"> + <!-- Email --> + <a href=`mailto:${general.email}` + class="flex items-center space-x-3 group hover:bg-white/10 p-3 rounded-lg transition-all"> + <Icon name="heroicons-outline:mail" class="h-6 w-6" /> + <span class="text-sm group-hover:text-indigo-200 transition-colors">{general.email}</span> + </a> + + <!-- WhatsApp --> + <a href=`https://wa.me/${general.phone}` + class="flex items-center space-x-3 group hover:bg-white/10 p-3 rounded-lg transition-all"> + <Icon name="ri:whatsapp-fill" class="h-6 w-6" /> + <span class="text-sm group-hover:text-indigo-200 transition-colors">WhatsApp Message</span> + </a> + + <!-- Social Links --> + <div class="pt-4 border-t border-white/20"> + <div class="flex flex-wrap gap-2"> + {general.social_links.data.general.map((link: any) => ( + <a href={link.url} + class="p-2 hover:bg-white/10 rounded-lg transition-all flex items-center gap-2" + title={link.title}> + {link.title === "BuFai7an.Live" ? ( + <Image + src={cleanIconPath} + width={20} + height={20} + alt={link.title} + class="h-5 w-5" + /> + ) : ( + <Icon name={`simple-icons:${link.title.toLowerCase()}`} class="h-5 w-5" /> + )} + </a> + ))} + </div> + </div> </div> - <div class="mt-3 text-center"> - <p class="text-sm text-gray-600 dark:text-gray-400">I'll try to get back to you in 2-5 human days.</p> + <!-- Additional Info --> + <div class="mt-8 pt-6 border-t border-white/20"> + <p class="text-sm text-indigo-100"> + Based in <b>Riyadh, Saudi Arabia</b> + </p> </div> - </form> + </div> </div> - <!-- End Card --> + </div> </section> -<!-- End Contact Us --> diff --git a/src/components/Footer/Footer.astro b/src/components/Footer/Footer.astro index 1bfe3fc..6e83034 100644 --- a/src/components/Footer/Footer.astro +++ b/src/components/Footer/Footer.astro @@ -1,6 +1,6 @@ --- import Contact from "./Contact.astro"; -import { general } from '../../assets/data/db.json'; +import general from '../../assets/data/general.json'; import { Icon } from 'astro-icon' diff --git a/src/components/Footer/InputField.astro b/src/components/Footer/InputField.astro index e9a53bc..2c4ffa3 100644 --- a/src/components/Footer/InputField.astro +++ b/src/components/Footer/InputField.astro @@ -8,32 +8,36 @@ export interface Props { disabled?: boolean; readonly?: boolean; autofocus?: boolean; - } -const { name, type = 'text', label, placeholder='', required = true, disabled = false, readonly = false, autofocus = false } = Astro.props; +const { + name, + type = 'text', + label, + placeholder = '', + required = true, + disabled = false, + readonly = false, + autofocus = false +} = Astro.props; --- -<div> - <label for=`hs-${ name }-contact` class="block text-sm text-gray-700 font-medium dark:text-white mb-1">{ label }</label> +<div class="relative"> + <label + for={`field-${name}`} + class="block text-sm font-medium text-gray-700 dark:text-white mb-2" + > + {label} + </label> <input - type={ type } - name=`hs-${ name }-contact` - id=`hs-${ name }-contact` - class=" - py-3 px-4 - block - w-full - border-gray-200 - - rounded-md - text-sm - focus:border focus:border-blue-500 focus:ring-blue-500 - dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:border" - placeholder={ placeholder } - required={ required } - disabled={ disabled } - readonly={ readonly } - autofocus={ autofocus } + type={type} + name={name} + id={`field-${name}`} + class="w-full px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 bg-white dark:bg-slate-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-20 placeholder-gray-400 dark:placeholder-gray-500 transition-colors duration-200" + placeholder={placeholder} + required={required} + disabled={disabled} + readonly={readonly} + autofocus={autofocus} /> </div> \ No newline at end of file diff --git a/src/components/Header/Header.astro b/src/components/Header/Header.astro index 04b1220..69e6e8d 100644 --- a/src/components/Header/Header.astro +++ b/src/components/Header/Header.astro @@ -1,11 +1,11 @@ --- import mfo_picture from '../../assets/images/mfo_picture.jpg'; import { Image } from '@astrojs/image/components'; -import { general } from '../../assets/data/db.json'; +import general from '../../assets/data/general.json'; import Tags from './Tags.astro'; --- -<header class="mb-10 +<header class="mb-10 text-center overflow-hidden before:absolute before:top-0 before:left-1/2 before:bg-[url('/assets/images/polygon.svg')] @@ -14,8 +14,8 @@ import Tags from './Tags.astro'; dark:before:bg-[url('/assets/images/polygon-dark.svg')] "> - <div class="flex flex-col items-start justify-center text-center md:justify-start md:text-start md:align-bottom mb-10 md:flex-row"> - <Image class="rounded w-24 h-24 me-3" src={mfo_picture} alt="Mohannad Otaibi Picture" format="webp" /> + <div class="flex flex-col items-center justify-center text-center mb-10"> + <Image class="rounded w-24 h-24 mb-4" src={mfo_picture} alt="Mohannad Otaibi Picture" format="webp" /> <div class="title"> <h1 class="text-4xl md:text-5xl font-extrabold m-0 mb-2">{general.title}</h1> <p class="text-slate-500 dark:text-slate-400">{general.description}</p> @@ -25,11 +25,5 @@ import Tags from './Tags.astro'; <Tags tags={general.social_links.data} /> - - <div class="lead mt-10"> - <p class="text-lg font-normal text-gray-500 lg:text-xl dark:text-gray-400"> - Welcome to my <b>simple</b> <b>humble</b> website..<br /> - Here, you may get to know me a little bit better by knowing the <code class="text-base">things</code> I use, languages I speak, things I do or enjoy.. - </p> - </div> + </header> \ No newline at end of file diff --git a/src/components/Header/Tag.astro b/src/components/Header/Tag.astro index d9c1622..6d9468d 100644 --- a/src/components/Header/Tag.astro +++ b/src/components/Header/Tag.astro @@ -25,11 +25,10 @@ imagePath = imagePath?.replaceAll('\\','/').replaceAll('public', ''); hover:scale-110 transition-all text-xs font-normal lg:text-sm lg:font-medium - + border border-gray-200 dark:border-gray-600 inline-flex items-center px-2.5 py-0.5 - rounded group"> <div class="inline-flex items-center"> diff --git a/src/components/Header/Tags.astro b/src/components/Header/Tags.astro index 33e6e37..04868f0 100644 --- a/src/components/Header/Tags.astro +++ b/src/components/Header/Tags.astro @@ -1,7 +1,7 @@ --- import Tag from './Tag.astro'; import CvButton from './Button.astro'; -import { general } from '../../assets/data/db.json'; +import general from '../../assets/data/general.json'; export interface Props { tags: any; } @@ -9,14 +9,14 @@ export interface Props { const { tags } = Astro.props; --- -<div class="badges text-center lg:text-start"> - <div class="general"> +<div class="badges flex flex-col items-center justify-center"> + <div class="general flex flex-wrap justify-center"> { tags.general.map((link: any) => ( <Tag url={link.url} title={link.title} /> )) } </div> - <div class="technical"> + <div class="technical flex flex-wrap justify-center"> { tags.technical.map((link: any) => ( <Tag url={link.url} title={link.title} /> )) } diff --git a/src/components/Quote.astro b/src/components/Quote.astro index c240017..1e90706 100644 --- a/src/components/Quote.astro +++ b/src/components/Quote.astro @@ -9,27 +9,33 @@ const { text, author, source } = Astro.props; --- -<blockquote class=" -relative w-11/12 mb-10 hover:scale-110 transition-all mx-auto py-4 last:border-0 border-b border-dashed border-spacing-3 border-zinc-500"> - <svg class="absolute top-0 left-0 transform -translate-x-6 -translate-y-3 h-16 w-16 text-gray-100 dark:text-gray-700" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> - <path d="M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z" fill="currentColor"/> - </svg> - - <div class="relative z-10"> - <p class="text-gray-800 sm:text-xl dark:text-white"><em> - {text} - </em></p> - </div> - - <footer class="mt-6"> - <div class="flex items-center justify-end lg:justify-start"> - <div class="flex-shrink-0"> - </div> - <div class="lg:ms-4"> - <div class="text-base font-semibold text-gray-800 dark:text-gray-400">{author}</div> - <div class="text-xs text-gray-500 dark:text-gray-400">{source}</div> +<div class="group relative p-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 ease-in-out transform hover:-translate-y-1"> + <!-- Decorative quote mark --> + <div class="absolute top-4 left-4 text-gray-200 dark:text-gray-700 opacity-50 text-4xl font-serif">"</div> + + <!-- Quote text --> + <div class="relative"> + <p class="text-gray-700 dark:text-gray-300 text-lg leading-relaxed mb-4 pl-6"> + {text} + </p> + + <!-- Author and source --> + <div class="flex items-center justify-end mt-4 space-x-2"> + <div class="text-right"> + <div class="font-medium text-gray-900 dark:text-gray-100 text-sm"> + {author} + </div> + {source && ( + <div class="text-gray-500 dark:text-gray-400 text-xs"> + {source} + </div> + )} + </div> </div> - </div> - </footer> - </blockquote> + </div> + + <!-- Decorative elements --> + <div class="absolute inset-0 border border-gray-100 dark:border-gray-700 rounded-xl pointer-events-none"></div> + <div class="absolute inset-0 bg-gradient-to-br from-transparent to-gray-50 dark:to-gray-800/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-xl pointer-events-none"></div> +</div> \ No newline at end of file diff --git a/src/components/Sections/GeneralSection.astro b/src/components/Sections/GeneralSection.astro index ae618d4..4b40e91 100644 --- a/src/components/Sections/GeneralSection.astro +++ b/src/components/Sections/GeneralSection.astro @@ -36,7 +36,7 @@ const mdGridSizes: any = { 10: 'md:grid-cols-10', } -console.log(content.title, xlColSize, colSize) +console.log(`${content.title.padEnd(25)} ${xlColSize.toString().padEnd(5)} ${colSize}`); --- <section class="my-20"> diff --git a/src/components/Sections/Link.astro b/src/components/Sections/Link.astro index 792f674..8eea5b7 100644 --- a/src/components/Sections/Link.astro +++ b/src/components/Sections/Link.astro @@ -18,7 +18,7 @@ const direction = arabic ? 'rtl' : 'ltr'; --- <a href={url} class=" -bg-slate-500 bg-opacity-10 +bg-slate-500 bg-opacity-10 border-slate-200 border-opacity-10 border hover:bg-purple-400 hover:bg-opacity-10 dark:bg-white dark:bg-opacity-5 dark:hover:bg-opacity-10 rounded-md @@ -35,5 +35,8 @@ hover:bg-purple-400 hover:bg-opacity-10 alt={title} /> <h3 dir={direction} class="font-normal text text-slate-700 dark:text-slate-300">{title}</h3> + {Astro.props.description && ( + <span class="ms-2 text-sm text-slate-500 dark:text-slate-400">{Astro.props.description}</span> + )} </div> </a> \ No newline at end of file diff --git a/src/components/Sections/MapSection.astro b/src/components/Sections/MapSection.astro index 092940b..25de879 100644 --- a/src/components/Sections/MapSection.astro +++ b/src/components/Sections/MapSection.astro @@ -10,11 +10,130 @@ const stringified = JSON.stringify(content.data); --- <section class="my-20"> - <Title title={content.title} description={content.description} /> + <div class="max-w-7xl mx-auto"> + <Title title={content.title} description={content.description} /> + </div> + - <div class='rounded-md overflow-hidden'> - <input type="hidden" data-message={stringified} /> - <div id="map" class="w-full h-80"></div> + <div class='overflow-hidden'> + <input type="hidden" id="map-data" data-message={stringified} /> + <div id="map" class="w-full h-[50vh]"></div> </div> </section> -<script src="../../utilities/googleMaps.ts"></script> \ No newline at end of file + +<!-- Add Leaflet CSS --> +<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" + integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" + crossorigin=""/> + +<!-- Add Leaflet JavaScript --> +<script is:inline src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" + integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" + crossorigin=""></script> + +<script> + declare const L: any; + // Get the map data + const mapData = document.getElementById('map-data'); + if (!mapData) throw new Error('Map data element not found'); + const message = mapData.dataset.message; + if (!message) throw new Error('Map data message not found'); + const locations = JSON.parse(message); + + // Initialize the map + const map = L.map('map', { + zoomControl: false, + center: [25, 45], + zoom: 15, + minZoom: 2, + maxZoom: 18, + worldCopyJump: true, + fadeAnimation: true, + markerZoomAnimation: true, + inertia: true, + inertiaDeceleration: 3000, + scrollWheelZoom: false + }).setView([25, 45], 3); + + // Add custom zoom control + L.control.zoom({ + position: 'topleft', + + }).addTo(map); + + + + // Add Mapbox tiles with Swarm's style + L.tileLayer('https://api.mapbox.com/styles/v1/foursquare/ck7qbe9t20y6v1iqkyeolw8hk/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoiZm91cnNxdWFyZSIsImEiOiJjRGRqOVZZIn0.rMLhJeqI_4VnU2YdIJvD3Q', { + attribution: '© Mapbox © OpenStreetMap', + maxZoom: 18, + minZoom: 2, + opacity: 1, + detectRetina: true, + '2x': true + }).addTo(map); + + // Custom marker icon (orange dot like Swarm) + const customIcon = L.divIcon({ + className: 'custom-map-marker', + html: '<div></div>', + iconSize: new L.Point(8,8), + iconAnchor: new L.Point(4,4) + }); + + // Add markers for each location + locations.forEach((location: { venueId: string; latLng: { lat: number; lng: number } }) => { + const marker = L.marker([location.latLng.lat, location.latLng.lng], { + icon: customIcon + }).addTo(map); + + // Add click handler to open Foursquare venue page + if (!location.venueId.startsWith('no_venue_id')) { + marker.on('click', () => { + window.open(`https://foursquare.com/v/${location.venueId}`, '_blank'); + }); + } + }); + + // Fit bounds to show all markers + if (locations.length > 0) { + const bounds = L.latLngBounds( + locations.map( + (loc: { + latLng: { + lat: number; + lng: number + } + }) => [loc.latLng.lat, loc.latLng.lng])); + map.fitBounds(bounds, { padding: [0, 0] }); + } +</script> + +<style is:inline> + .custom-map-marker { + background: transparent; + } + .custom-map-marker div { + width: 8px; + height: 8px; + background: #fc8c14 ; + border-radius: 50%; + opacity: 1; + } + + .leaflet-popup-content-wrapper { + border-radius: 4px; + padding: 8px 12px; + font-size: 14px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + } + + .leaflet-control-zoom a { + width: 24px !important; + height: 24px !important; + line-height: 24px !important; + font-size: 16px !important; + } + + +</style> \ No newline at end of file diff --git a/src/components/Sections/QuotesSection.astro b/src/components/Sections/QuotesSection.astro index 10952ed..3210b4a 100644 --- a/src/components/Sections/QuotesSection.astro +++ b/src/components/Sections/QuotesSection.astro @@ -1,20 +1,24 @@ --- import Quote from '../Quote.astro'; import Title from './Title.astro'; + export interface Props { content: any; xlColSize?: number; colSize?: number; } -const { content, xlColSize=1, colSize=1 } = Astro.props; -console.log(content.title, xlColSize, colSize) + +const { content, xlColSize=2, colSize=1 } = Astro.props; --- -<section class="my-20"> +<section class="my-12"> <Title title={content.title} description={content.description} /> - <div class=`grid grid-cols-${colSize} md:grid-cols-${xlColSize} gap-2 md:gap-4`> - {content.data.map((item: any) => ( - <Quote text={item.text} author={item.author} source={item.source} /> - ))} + <div class="container mx-auto px-4"> + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8"> + {content.data.map((item: any) => ( + <Quote text={item.text} author={item.author} source={item.source} /> + ))} + </div> + </div> </section> \ No newline at end of file diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index f4147c3..9aa98fd 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -116,7 +116,8 @@ const { title, description } = Astro.props; <slot /> </div> - </body><style is:global> + </body> + <style is:global> code { @apply font-normal; @apply bg-indigo-700 bg-opacity-40 text-indigo-700 dark:text-indigo-100; diff --git a/src/pages/index.astro b/src/pages/index.astro index 1d3cca4..d8b011e 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,10 +1,19 @@ --- import Layout from '../layouts/Layout.astro'; -import Card from '../components/Common/Card.astro'; import Header from '../components/Header/Header.astro'; import Footer from '../components/Footer/Footer.astro'; import Section from '../components/Sections/GeneralSection.astro'; -import { general, linked_sections, quotes, locations } from '../assets/data/db.json'; +import operating_systems from '../assets/data/linked/operating_systems.json'; +import organizations from '../assets/data/linked/organizations.json'; +import languages from '../assets/data/linked/languages.json'; +import achievements from '../assets/data/linked/achievements.json'; +import bookmarks from '../assets/data/linked/bookmarks.json'; +import applications from '../assets/data/linked/applications.json'; +import standards from '../assets/data/linked/standards.json'; +import games from '../assets/data/linked/games.json'; +import general from '../assets/data/general.json'; +import quotes from '../assets/data/quotes.json'; +import locations from '../assets/data/locations.json'; import QuotesSection from '../components/Sections/QuotesSection.astro'; import MapSection from '../components/Sections/MapSection.astro'; @@ -12,20 +21,25 @@ import MapSection from '../components/Sections/MapSection.astro'; --- <Layout title={general.title} description={general.description}> - <main class="m-auto px-6 max-w-5xl"> + <main class=""> + <div class="max-w-7xl mx-auto"> <Header /> - <Section content={linked_sections.applications} /> - <Section content={linked_sections.operating_systems} /> - <Section content={linked_sections.languages} /> - <Section content={linked_sections.achievements} colSize={1} xlColSize={1} /> - <Section content={linked_sections.standards} /> - <Section content={linked_sections.organizations} /> - <Section content={linked_sections.bookmarks} colSize={1} xlColSize={2} /> - <Section content={linked_sections.games} colSize={1} xlColSize={2} /> - + <Section content={applications} /> + <Section content={operating_systems} colSize={1} xlColSize={2} /> + <Section content={languages} /> + <Section content={achievements} colSize={1} xlColSize={1} /> + <Section content={standards} /> + <Section content={organizations} /> + <Section content={bookmarks} colSize={1} xlColSize={3} /> + <Section content={games} colSize={1} xlColSize={2} /> <QuotesSection content={quotes} /> + </div> + + + <MapSection content={locations} /> - + <div class="max-w-7xl mx-auto"> <Footer /> + </div> </main> </Layout> diff --git a/src/utilities/getIcon.ts b/src/utilities/getIcon.ts index b36c87d..05272af 100644 --- a/src/utilities/getIcon.ts +++ b/src/utilities/getIcon.ts @@ -6,89 +6,173 @@ import path from "path"; import icojs from 'icojs'; import sharp from 'sharp'; +// Configuration +const CONFIG = { + ICON_SIZE: 64, + CACHE_DIR: "public/assets/images/icons", + DUMMY_ICON: "public/assets/images/icons/generic.png", + MAX_RETRIES: 3, + RETRY_DELAY: 1000, + PARALLEL_LIMIT: 3 +}; -const favIconGrabberUrls = [ - "https://www.google.com/s2/favicons?sz=64&domain_url=", - "https://api.faviconkit.com/", - "https://favicongrabber.com/api/grab/", - "https://www.google.com/s2/u/0/favicons?domain=", - "https://favicon.mohannad-otaibi.workers.dev/convert?format=png&url=", +// Favicon sources with placeholders for domain +const FAVICON_SOURCES = [ + { + url: "https://www.google.com/s2/favicons?sz=64&domain_url=", + appendDomain: true, + appendProtocol: true + }, + { + url: "https://api.faviconkit.com/", + appendDomain: true, + appendProtocol: false + }, + { + url: "https://favicongrabber.com/api/grab/", + appendDomain: true, + appendProtocol: false + }, + { + url: "https://favicon.mohannad-otaibi.workers.dev/convert?format=png&url=", + appendDomain: true, + appendProtocol: true + } ]; +// Axios instance with configuration const axiosInstance = axios.create({ maxRedirects: 35, + timeout: 5000, httpsAgent: new https.Agent({ rejectUnauthorized: false, }), }); -async function copyDummy(domain: string) { - const urlObject = new URL(domain); - const dummy = "public/assets/images/icons/generic.png"; - const targetDirectory = "public/assets/images/icons"; - const iconPath = path.join(targetDirectory, `${urlObject.hostname}.png`); - - await fsPromisified.copyFile(dummy, iconPath); - return iconPath; +// URL validation +function isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } } -async function downloadIcon(iconUrl: string, iconPath: string) { - const response = await axiosInstance.get(iconUrl, { responseType: "arraybuffer" }); - const buffer = Buffer.from(response.data); - - // check if the file is ico file by checking the stream or buffer content, if yes, convert to png - const isIco = await icojs.isICO(buffer); - console.log("isIco", isIco); - if (isIco) { - - console.log("Converting ico to png"); - const frames = await icojs.parse(buffer); - const firstFrame = frames[0]; - - const pngBuffer = await sharp(firstFrame.buffer).png().toBuffer(); +// Delay utility for retry mechanism +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - // Save the PNG buffer as a file - await sharp(pngBuffer).toFile(iconPath); - - } - else{ - await fsPromisified.writeFile(iconPath, buffer); +// Retry mechanism with exponential backoff +async function withRetry<T>( + fn: () => Promise<T>, + retries = CONFIG.MAX_RETRIES, + initialDelay = CONFIG.RETRY_DELAY +): Promise<T> { + try { + return await fn(); + } catch (error) { + if (retries === 0) throw error; + await delay(initialDelay); + return withRetry(fn, retries - 1, initialDelay * 2); } - } -async function getIcon(url: string) { - if (url === "#") { - return "public/assets/images/icons/generic.png"; - } +// Image processing utilities +async function normalizeImage(buffer: Buffer, iconPath: string): Promise<void> { + await sharp(buffer) + .resize(CONFIG.ICON_SIZE, CONFIG.ICON_SIZE, { + fit: 'contain', + background: { r: 0, g: 0, b: 0, alpha: 0 } + }) + .png() + .toFile(iconPath); +} - const targetDirectory = "public/assets/images/icons"; - const urlObject = new URL(url); - const { origin, hostname } = urlObject; - const iconPath = path.join(targetDirectory, `${hostname}.png`); +async function processIcoFile(buffer: Buffer): Promise<Buffer> { + const frames = await icojs.parse(buffer); + const bestFrame = frames.reduce((prev, curr) => + (curr.width > prev.width) ? curr : prev + ); + return await sharp(bestFrame.buffer).png().toBuffer(); +} - if (fs.existsSync(iconPath)) { - return iconPath; +// Download and process icon +async function downloadIcon(iconUrl: string, iconPath: string): Promise<void> { + const response = await withRetry(() => + axiosInstance.get(iconUrl, { responseType: "arraybuffer" }) + ); + + const buffer = Buffer.from(response.data); + + try { + if (await icojs.isICO(buffer)) { + const pngBuffer = await processIcoFile(buffer); + await normalizeImage(pngBuffer, iconPath); + } else { + await normalizeImage(buffer, iconPath); } + } catch (error) { + console.error("Error processing image:", error); + throw error; + } +} - try { - const iconUrl = `${favIconGrabberUrls[0]}${origin}`; - await downloadIcon(iconUrl, iconPath); - return iconPath; - } - catch (error: any) { - console.error("Error downloading icon:", iconPath, error.code, error.message); - try { - const iconUrl = `${origin}/favicon.ico`; - await downloadIcon(iconUrl, iconPath); - return iconPath; - } - catch (error: any) { - console.error("Error downloading fallback icon:", iconPath, error.code, error.message); - return copyDummy(url); - } +// Try to fetch icon from multiple sources in parallel +async function tryFetchIcon(url: string, iconPath: string): Promise<string> { + const urlObject = new URL(url); + const sources = FAVICON_SOURCES.map(source => { + const domain = source.appendProtocol ? urlObject.origin : urlObject.hostname; + return source.url + domain; + }); + // Add website's own favicon.ico + sources.push(`${urlObject.origin}/favicon.ico`); + + // Try sources in parallel with a limit + for (let i = 0; i < sources.length; i += CONFIG.PARALLEL_LIMIT) { + const batch = sources.slice(i, i + CONFIG.PARALLEL_LIMIT); + const promises = batch.map(source => + downloadIcon(source, iconPath) + .then(() => true) + .catch(() => false) + ); + + const results = await Promise.all(promises); + if (results.some(result => result)) { + return iconPath; } + } + + throw new Error("No favicon found from any source"); +} + +// Main getIcon function +async function getIcon(url: string): Promise<string> { + // Handle special cases + if (url === "#" || !isValidUrl(url)) { + return CONFIG.DUMMY_ICON; + } + + const urlObject = new URL(url); + const iconPath = path.join(CONFIG.CACHE_DIR, `${urlObject.hostname}.png`); + + // Return cached icon if exists + if (fs.existsSync(iconPath)) { + return iconPath; + } + + try { + // Ensure cache directory exists + await fsPromisified.mkdir(CONFIG.CACHE_DIR, { recursive: true }); + + // Try to fetch and process icon + return await tryFetchIcon(url, iconPath); + } catch (error) { + console.error("Failed to fetch icon for:", url, error); + // Copy dummy icon as fallback + await fsPromisified.copyFile(CONFIG.DUMMY_ICON, iconPath); + return iconPath; + } } export default getIcon;