Merge pull request #17 from juancmandev/dev
Website redesing, translation to Spanish, other improvements
This commit is contained in:
commit
07d436fda9
@ -17,8 +17,6 @@
|
|||||||
"@astrojs/sitemap": "^3.1.6",
|
"@astrojs/sitemap": "^3.1.6",
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astrojs/vercel": "^7.7.2",
|
"@astrojs/vercel": "^7.7.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@tailwindcss/typography": "^0.5.13",
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
444
pnpm-lock.yaml
generated
444
pnpm-lock.yaml
generated
@ -29,12 +29,6 @@ importers:
|
|||||||
'@astrojs/vercel':
|
'@astrojs/vercel':
|
||||||
specifier: ^7.7.2
|
specifier: ^7.7.2
|
||||||
version: 7.7.2(astro@4.11.3(typescript@5.5.2))(react@18.3.1)
|
version: 7.7.2(astro@4.11.3(typescript@5.5.2))(react@18.3.1)
|
||||||
'@radix-ui/react-dialog':
|
|
||||||
specifier: ^1.1.1
|
|
||||||
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-scroll-area':
|
|
||||||
specifier: ^1.1.0
|
|
||||||
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
version: 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||||
@ -635,12 +629,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0':
|
|
||||||
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.0':
|
|
||||||
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
|
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.0':
|
'@radix-ui/react-compose-refs@1.1.0':
|
||||||
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
|
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -650,133 +638,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-context@1.1.0':
|
|
||||||
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.1':
|
|
||||||
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.0':
|
|
||||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.0':
|
|
||||||
resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.1.0':
|
|
||||||
resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.0':
|
|
||||||
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-id@1.1.0':
|
|
||||||
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.1':
|
|
||||||
resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-presence@1.1.0':
|
|
||||||
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.0.0':
|
|
||||||
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-scroll-area@1.1.0':
|
|
||||||
resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.1.0':
|
'@radix-ui/react-slot@1.1.0':
|
||||||
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
|
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -786,42 +647,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.0':
|
|
||||||
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-use-controllable-state@1.1.0':
|
|
||||||
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-use-escape-keydown@1.1.0':
|
|
||||||
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.0':
|
|
||||||
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rollup/pluginutils@4.2.1':
|
'@rollup/pluginutils@4.2.1':
|
||||||
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@ -1118,10 +943,6 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
aria-hidden@1.2.4:
|
|
||||||
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
aria-query@5.3.0:
|
aria-query@5.3.0:
|
||||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||||
|
|
||||||
@ -1357,9 +1178,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-node-es@1.1.0:
|
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
|
||||||
|
|
||||||
deterministic-object-hash@2.0.2:
|
deterministic-object-hash@2.0.2:
|
||||||
resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
|
resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -1555,10 +1373,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
|
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
get-nonce@1.0.1:
|
|
||||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
get-stream@8.0.1:
|
get-stream@8.0.1:
|
||||||
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -1687,9 +1501,6 @@ packages:
|
|||||||
inline-style-parser@0.2.3:
|
inline-style-parser@0.2.3:
|
||||||
resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==}
|
resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==}
|
||||||
|
|
||||||
invariant@2.2.4:
|
|
||||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
|
||||||
|
|
||||||
is-alphabetical@2.0.1:
|
is-alphabetical@2.0.1:
|
||||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||||
|
|
||||||
@ -2379,36 +2190,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.6:
|
|
||||||
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
react-remove-scroll@2.5.7:
|
|
||||||
resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
react-style-singleton@2.2.1:
|
|
||||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2780,26 +2561,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
use-callback-ref@1.3.2:
|
|
||||||
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
use-sidecar@1.1.2:
|
|
||||||
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@ -3608,133 +3369,12 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0': {}
|
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.0': {}
|
|
||||||
|
|
||||||
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.3
|
'@types/react': 18.3.3
|
||||||
|
|
||||||
'@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/primitive': 1.1.0
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
aria-hidden: 1.2.4
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
react-remove-scroll: 2.5.7(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-direction@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/primitive': 1.1.0
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-scroll-area@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/number': 1.1.0
|
|
||||||
'@radix-ui/primitive': 1.1.0
|
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
'@types/react-dom': 18.3.0
|
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
'@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||||
@ -3742,32 +3382,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.3
|
'@types/react': 18.3.3
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
'@rollup/pluginutils@4.2.1':
|
'@rollup/pluginutils@4.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
@ -4075,10 +3689,6 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
aria-hidden@1.2.4:
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.6.3
|
|
||||||
|
|
||||||
aria-query@5.3.0:
|
aria-query@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@ -4363,8 +3973,6 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.0.3: {}
|
detect-libc@2.0.3: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
|
||||||
|
|
||||||
deterministic-object-hash@2.0.2:
|
deterministic-object-hash@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
base-64: 1.0.0
|
base-64: 1.0.0
|
||||||
@ -4581,8 +4189,6 @@ snapshots:
|
|||||||
|
|
||||||
get-east-asian-width@1.2.0: {}
|
get-east-asian-width@1.2.0: {}
|
||||||
|
|
||||||
get-nonce@1.0.1: {}
|
|
||||||
|
|
||||||
get-stream@8.0.1: {}
|
get-stream@8.0.1: {}
|
||||||
|
|
||||||
github-slugger@2.0.0: {}
|
github-slugger@2.0.0: {}
|
||||||
@ -4806,10 +4412,6 @@ snapshots:
|
|||||||
|
|
||||||
inline-style-parser@0.2.3: {}
|
inline-style-parser@0.2.3: {}
|
||||||
|
|
||||||
invariant@2.2.4:
|
|
||||||
dependencies:
|
|
||||||
loose-envify: 1.4.0
|
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
|
|
||||||
is-alphanumerical@2.0.1:
|
is-alphanumerical@2.0.1:
|
||||||
@ -5700,34 +5302,6 @@ snapshots:
|
|||||||
|
|
||||||
react-refresh@0.14.2: {}
|
react-refresh@0.14.2: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
tslib: 2.6.3
|
|
||||||
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
get-nonce: 1.0.1
|
|
||||||
invariant: 2.2.4
|
|
||||||
react: 18.3.1
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@ -6147,7 +5721,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.2
|
typescript: 5.5.2
|
||||||
|
|
||||||
tslib@2.6.3: {}
|
tslib@2.6.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
type-fest@2.19.0: {}
|
type-fest@2.19.0: {}
|
||||||
|
|
||||||
@ -6225,21 +5800,6 @@ snapshots:
|
|||||||
escalade: 3.1.2
|
escalade: 3.1.2
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
|
|
||||||
use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
detect-node-es: 1.1.0
|
|
||||||
react: 18.3.1
|
|
||||||
tslib: 2.6.3
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.3
|
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vfile-location@5.0.2:
|
vfile-location@5.0.2:
|
||||||
|
@ -2,16 +2,33 @@ import { Code, RssIcon } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import formatDate from "@/utils/format-date";
|
import formatDate from "@/utils/format-date";
|
||||||
|
|
||||||
export default function Footer() {
|
const locales = {
|
||||||
|
en: {
|
||||||
|
developed_by: "Developed by",
|
||||||
|
build_handcrafted: "Built handcrafted with",
|
||||||
|
last_build: "Last build",
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
developed_by: "Desarrollado por",
|
||||||
|
build_handcrafted: "Construido a mano con",
|
||||||
|
last_build: "Última build",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
lang: "en" | "es";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Footer(props: Props) {
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-secondary px-4 py-12 text-center text-sm md:px-16 prose prose-invert min-w-full">
|
<footer className="border-t border-secondary px-4 py-12 text-center text-sm md:px-16 prose prose-invert min-w-full">
|
||||||
<section>
|
<section>
|
||||||
<p>
|
<p>
|
||||||
Developed by{" "}
|
{locales[props.lang].developed_by}{" "}
|
||||||
<strong className="font-bold text-primary">juancmandev</strong>
|
<strong className="font-bold text-primary">juancmandev</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Built handcrafted with{" "}
|
{locales[props.lang].build_handcrafted}{" "}
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size={null}
|
size={null}
|
||||||
@ -23,7 +40,10 @@ export default function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
<p>Last built {formatDate(new Date())}.</p>
|
<p>
|
||||||
|
{locales[props.lang].last_build}: {formatDate(new Date(), props.lang)}
|
||||||
|
.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section className="w-max mx-auto flex items-center gap-12">
|
<section className="w-max mx-auto flex items-center gap-12">
|
||||||
<Button
|
<Button
|
||||||
@ -43,7 +63,14 @@ export default function Footer() {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="flex flex-col justify-center"
|
className="flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a target="_blank" href="https://juancman.dev/rss.xml">
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={
|
||||||
|
props.lang == "en"
|
||||||
|
? "https://juancman.dev/feed.xml"
|
||||||
|
: "https://juancman.dev/es/feed.xml"
|
||||||
|
}
|
||||||
|
>
|
||||||
<RssIcon className="w-6" />
|
<RssIcon className="w-6" />
|
||||||
RSS feed
|
RSS feed
|
||||||
</a>
|
</a>
|
||||||
|
70
src/components/header.astro
Normal file
70
src/components/header.astro
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import logo from "@/assets/logo.png";
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import LinkButton from "@/components/link-button";
|
||||||
|
import { ChevronUp, Compass } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
lang: "en" | "es";
|
||||||
|
};
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
en: {
|
||||||
|
to: "/es",
|
||||||
|
switch_language: "🇲🇽",
|
||||||
|
top: "Top",
|
||||||
|
navigation: "Navigation",
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
to: "/",
|
||||||
|
switch_language: "🇺🇸",
|
||||||
|
top: "Arriba",
|
||||||
|
navigation: "Navevación",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lang } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
class="py-2 fixed top-0 z-50 flex w-full items-center justify-between border-b border-secondary backdrop-blur-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="px-4 sm:px-0 flex w-full max-w-[65ch] items-center justify-between mx-auto"
|
||||||
|
>
|
||||||
|
<section class="flex max-w-max">
|
||||||
|
<LinkButton
|
||||||
|
href={lang === "en" ? "/" : "/es"}
|
||||||
|
size="icon"
|
||||||
|
variant="link"
|
||||||
|
className="rounded-full px-0"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
loading="eager"
|
||||||
|
class="w-auto h-auto"
|
||||||
|
alt="juancmandev logo"
|
||||||
|
/>
|
||||||
|
</LinkButton>
|
||||||
|
</section>
|
||||||
|
<section class="flex items-center gap-2">
|
||||||
|
<LinkButton
|
||||||
|
variant="link"
|
||||||
|
href={locales[lang].to}
|
||||||
|
className="p-0 gap-1 text-base"
|
||||||
|
>
|
||||||
|
{locales[lang].switch_language}
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton variant="link" className="p-0 gap-0.5" href="#">
|
||||||
|
<ChevronUp className="w-5" />
|
||||||
|
{locales[lang].top}
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton variant="link" className="p-0 gap-0.5" href="#navigation">
|
||||||
|
<Compass className="w-5" />
|
||||||
|
{locales[lang].navigation}
|
||||||
|
</LinkButton>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</header>
|
@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
href: string;
|
href: string;
|
||||||
variant?:
|
variant?:
|
||||||
| "default"
|
| "default"
|
||||||
@ -21,6 +22,7 @@ export default function LinkButton(props: Props) {
|
|||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size={props.size}
|
size={props.size}
|
||||||
|
title={props.title}
|
||||||
variant={props.variant}
|
variant={props.variant}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
>
|
>
|
||||||
|
@ -5,16 +5,16 @@ const { src, alt } = Astro.props;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
id="img"
|
id="img"
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
width={1092}
|
width={1092}
|
||||||
height={986}
|
height={986}
|
||||||
class=".markdown-image w-auto h-auto rounded-md aspect-auto"
|
class="w-auto h-auto rounded-md aspect-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const image = document.getElementById("img")!;
|
const image = document.getElementById("img")!;
|
||||||
|
|
||||||
image.setAttribute("loading", "eager");
|
image && image.setAttribute("loading", "eager");
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
||||||
|
|
||||||
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CopyButton(props: CopyButtonProps) {
|
|
||||||
const [hasCopied, setHasCopied] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setHasCopied(false);
|
|
||||||
}, 2000);
|
|
||||||
}, [hasCopied]);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback((value: string) => {
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
|
|
||||||
setHasCopied(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
title={hasCopied ? "Copied!" : "Copy to clipboard"}
|
|
||||||
size={null}
|
|
||||||
variant="ghost"
|
|
||||||
className={cn("absolute right-2 top-1.5 p-2 ", props.className)}
|
|
||||||
onClick={() => copyToClipboard(props.value)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Copy</span>
|
|
||||||
{hasCopied ? (
|
|
||||||
<CheckIcon className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<CopyIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +1,33 @@
|
|||||||
---
|
---
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import formatDate from "@/utils/format-date";
|
import formatDate from "@/utils/format-date";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
const props = Astro.props;
|
const props = Astro.props;
|
||||||
const content = marked.parse(props.content);
|
const content = marked.parse(props.content);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="rounded-md border px-4 py-2">
|
<article class="rounded-md border px-4 py-2">
|
||||||
<header class="mb-2">
|
<header class="mb-2">
|
||||||
<section class="flex items-center justify-between text-sm">
|
<section class="flex items-center justify-between text-sm">
|
||||||
<span class="font-light">
|
<span class="font-light">
|
||||||
{formatDate(new Date(props.published))}{" "}
|
{formatDate(new Date(props.published), lang)}{" "}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-thin">
|
<span class="text-sm font-thin">
|
||||||
{new Date(props.published).toLocaleTimeString()}
|
{new Date(props.published).toLocaleTimeString()}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
<section class="mt-1">
|
<section class="mt-1">
|
||||||
{
|
{
|
||||||
props &&
|
props &&
|
||||||
props.expand.tags &&
|
props.expand.tags &&
|
||||||
props?.expand.tags.map(
|
props?.expand.tags.map(
|
||||||
(tag: any) =>
|
(tag: any) => tag && <span class="text-sm">#{tag.name} </span>,
|
||||||
tag && <span class="text-sm">#{tag.name} </span>,
|
)
|
||||||
)
|
}
|
||||||
}
|
</section>
|
||||||
</section>
|
</header>
|
||||||
</header>
|
<main set:html={content} />
|
||||||
<main set:html={content} />
|
|
||||||
</article>
|
</article>
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
} from "@/components/ui/sheet";
|
|
||||||
import { MenuIcon } from "lucide-react";
|
|
||||||
import { navItems } from "@/utils/nav-links";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export default function MobileMenu() {
|
|
||||||
return (
|
|
||||||
<Sheet>
|
|
||||||
<SheetTrigger asChild title="Open menu">
|
|
||||||
<Button size="icon" variant="ghost">
|
|
||||||
<MenuIcon />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent
|
|
||||||
side="right"
|
|
||||||
className="w-max border-0 bg-secondary px-0 pt-14 shadow-2xl"
|
|
||||||
>
|
|
||||||
<SheetHeader>
|
|
||||||
<ScrollArea>
|
|
||||||
<nav className="h-[calc(100vh_-_100px)]">
|
|
||||||
<section className="mt-1 flex flex-col">
|
|
||||||
<ul className="flex flex-col gap-1">
|
|
||||||
{navItems.map((navItem) => (
|
|
||||||
<li key={navItem.label} className="flex h-max w-full">
|
|
||||||
<SheetClose asChild>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size={null}
|
|
||||||
variant="link"
|
|
||||||
className="w-full cursor-default rounded-none px-10 py-3 hover:bg-background/50 hover:no-underline focus:bg-background/50"
|
|
||||||
>
|
|
||||||
<a className="capitalize" href={navItem.to}>
|
|
||||||
{navItem.label}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</SheetClose>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
</ScrollArea>
|
|
||||||
</SheetHeader>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
import logo from "@/assets/logo.png";
|
|
||||||
import { Image } from "astro:assets";
|
|
||||||
import { navItems } from "@/utils/nav-links";
|
|
||||||
import MobileMenu from "@/components/mobile-menu";
|
|
||||||
import LinkButton from "@/components/link-button";
|
|
||||||
---
|
|
||||||
|
|
||||||
<nav
|
|
||||||
class="py-2 fixed top-0 z-50 flex w-full items-center justify-between border-b border-secondary backdrop-blur-lg"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="px-4 flex w-full max-w-[65ch] items-center justify-between mx-auto"
|
|
||||||
>
|
|
||||||
<section class="flex max-w-max">
|
|
||||||
<LinkButton
|
|
||||||
href="/"
|
|
||||||
size="icon"
|
|
||||||
variant="link"
|
|
||||||
className="rounded-full px-0"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={logo}
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
loading="eager"
|
|
||||||
class="w-auto h-auto"
|
|
||||||
alt="juancmandev logo"
|
|
||||||
/>
|
|
||||||
</LinkButton>
|
|
||||||
</section>
|
|
||||||
<section class="hidden items-center md:flex">
|
|
||||||
<ul class="flex items-center gap-1">
|
|
||||||
{
|
|
||||||
navItems.map((navItem: any) => (
|
|
||||||
<li class="w-max h-max">
|
|
||||||
<LinkButton
|
|
||||||
variant="link"
|
|
||||||
className="px-2"
|
|
||||||
href={navItem.to}
|
|
||||||
>
|
|
||||||
{navItem.label}
|
|
||||||
</LinkButton>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="flex h-max items-center md:hidden">
|
|
||||||
<MobileMenu client:idle />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
72
src/components/navigation.tsx
Normal file
72
src/components/navigation.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import LinkButton from "@/components/link-button";
|
||||||
|
import {
|
||||||
|
NotebookText,
|
||||||
|
BriefcaseBusiness,
|
||||||
|
MonitorPlay,
|
||||||
|
Newspaper,
|
||||||
|
PocketKnife,
|
||||||
|
Info,
|
||||||
|
Mail,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useTranslations } from "@/i18n/utils";
|
||||||
|
|
||||||
|
type TNavItem = {
|
||||||
|
type: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navItems: TNavItem[] = [
|
||||||
|
{
|
||||||
|
type: "blog",
|
||||||
|
icon: <NotebookText />,
|
||||||
|
},
|
||||||
|
{ type: "portfolio", icon: <BriefcaseBusiness /> },
|
||||||
|
{
|
||||||
|
type: "videos",
|
||||||
|
icon: <MonitorPlay />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "microblog",
|
||||||
|
icon: <Newspaper />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "resources",
|
||||||
|
icon: <PocketKnife />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "about",
|
||||||
|
icon: <Info />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "contact",
|
||||||
|
icon: <Mail />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
lang: "en" | "es";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Navigation(props: Props) {
|
||||||
|
const t = useTranslations(props.lang as any);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="px-4 sm:px-0 max-w-[65ch] mx-auto prose prose-invert pt-5 pb-20">
|
||||||
|
<h2 id="navigation">{t("navigation")}</h2>
|
||||||
|
<ul className="list-none p-0 flex flex-wrap gap-4">
|
||||||
|
{navItems.map((navItem, index) => (
|
||||||
|
<li key={index} className="m-0 p-0">
|
||||||
|
<LinkButton
|
||||||
|
variant="link"
|
||||||
|
href={t(`${navItem.type}.to` as any)}
|
||||||
|
className="p-0 text-base gap-1"
|
||||||
|
>
|
||||||
|
{navItem.icon}
|
||||||
|
{t(`${navItem.type}.label` as any)}
|
||||||
|
</LinkButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
@ -5,7 +5,8 @@ type Props = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
title: string;
|
title: string;
|
||||||
type: "blog" | "portfolio";
|
type: "blog" | "portfolio" | "es/videos";
|
||||||
|
lang: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PostItem(props: Props) {
|
export default function PostItem(props: Props) {
|
||||||
@ -16,9 +17,16 @@ export default function PostItem(props: Props) {
|
|||||||
variant="link"
|
variant="link"
|
||||||
className="px-4 whitespace-normal py-2 hover:no-underline focus:no-underline flex flex-col items-start italic border border-secondary hover:border-foreground focus:border-foreground transition-colors rounded-md"
|
className="px-4 whitespace-normal py-2 hover:no-underline focus:no-underline flex flex-col items-start italic border border-secondary hover:border-foreground focus:border-foreground transition-colors rounded-md"
|
||||||
>
|
>
|
||||||
<a className="no-underline" href={`/${props.type}/${props.slug}`}>
|
<a
|
||||||
|
className="no-underline"
|
||||||
|
href={
|
||||||
|
props.lang === "en"
|
||||||
|
? `/${props.type}/${props.slug}`
|
||||||
|
: `/es/${props.type}/${[props.slug]}`
|
||||||
|
}
|
||||||
|
>
|
||||||
<span className="text-sm font-light no-underline">
|
<span className="text-sm font-light no-underline">
|
||||||
{formatDate(props.date)}
|
{formatDate(props.date, props.lang)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-primary text-underline text-lg font-semibold underline">
|
<span className="text-primary text-underline text-lg font-semibold underline">
|
||||||
{props.title}
|
{props.title}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative overflow-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
||||||
{children}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
ref={ref}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
orientation === "vertical" &&
|
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
||||||
orientation === "horizontal" &&
|
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
))
|
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
|
@ -1,138 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Sheet = SheetPrimitive.Root
|
|
||||||
|
|
||||||
const SheetTrigger = SheetPrimitive.Trigger
|
|
||||||
|
|
||||||
const SheetClose = SheetPrimitive.Close
|
|
||||||
|
|
||||||
const SheetPortal = SheetPrimitive.Portal
|
|
||||||
|
|
||||||
const SheetOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Overlay
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const sheetVariants = cva(
|
|
||||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
||||||
bottom:
|
|
||||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
||||||
right:
|
|
||||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface SheetContentProps
|
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
||||||
VariantProps<typeof sheetVariants> {}
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
||||||
SheetContentProps
|
|
||||||
>(({ side = "right", className, children, ...props }, ref) => (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(sheetVariants({ side }), className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
))
|
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SheetHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
SheetHeader.displayName = "SheetHeader"
|
|
||||||
|
|
||||||
const SheetFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
SheetFooter.displayName = "SheetFooter"
|
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold text-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetPortal,
|
|
||||||
SheetOverlay,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
@ -53,8 +53,7 @@ you don't open the post link in your RSS Reader.
|
|||||||
The good thing is that almost every RSS Reader shows you content sorted by date,
|
The good thing is that almost every RSS Reader shows you content sorted by date,
|
||||||
not by a creepy algorithm that wants you to be mad.
|
not by a creepy algorithm that wants you to be mad.
|
||||||
|
|
||||||
[For my website](https://github.com/juancmandev/website/blob/main/scripts/rss.ts),
|
For my website I use a Node.js script that takes the `.mdx` files inside `content/blog` and
|
||||||
I use a Node.js script that takes the `.mdx` files inside `content/blog` and
|
|
||||||
`content/portfolio`, then generates the RSS Items, those with the `rss: true` in
|
`content/portfolio`, then generates the RSS Items, those with the `rss: true` in
|
||||||
the metadata.
|
the metadata.
|
||||||
|
|
||||||
@ -115,6 +114,7 @@ https://youtube.com/feeds/videos.xml?channel_id=[CHANNEL ID]
|
|||||||
- [Astronomic Picture of the Day (apod)](https://apod.com/feed.rss)
|
- [Astronomic Picture of the Day (apod)](https://apod.com/feed.rss)
|
||||||
- [Earth Science Picture of the Day (epod)](https://feeds2.feedburner.com/EarthSciencePictureoftheDay)
|
- [Earth Science Picture of the Day (epod)](https://feeds2.feedburner.com/EarthSciencePictureoftheDay)
|
||||||
- [Erick Murphy (cool guy)](https://ericmurphy.xyz/index.xml)
|
- [Erick Murphy (cool guy)](https://ericmurphy.xyz/index.xml)
|
||||||
|
- [Luke Smith](https://lukesmith.xyz/index.xml)
|
||||||
|
|
||||||
## More About RSS
|
## More About RSS
|
||||||
|
|
@ -1,12 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: I will not continue creating content in Spanish for my website
|
title: I will not continue creating content in Spanish for my website
|
||||||
description: I had the idea of maintaining my website in both English and Spanish; however, that is giving me some trouble, like taking more time to create content.
|
description:
|
||||||
|
I had the idea of maintaining my website in both English and Spanish; however,
|
||||||
|
that is giving me some trouble, like taking more time to create content.
|
||||||
tags: [Personal]
|
tags: [Personal]
|
||||||
image: /blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg
|
image: /blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg
|
||||||
imageCaption: Letters mixed
|
imageCaption: Letters mixed
|
||||||
date: 2024-2-20
|
date: 2024-2-20
|
||||||
author: Juan Manzanero
|
author: Juan Manzanero
|
||||||
rss: true
|
rss: false
|
||||||
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||

|

|
||||||
@ -15,7 +18,11 @@ _Mixed letters. Photo by
|
|||||||
on
|
on
|
||||||
[Unsplash](https://unsplash.com/photos/red-alphabet-decors-0sBTrm726C8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
[Unsplash](https://unsplash.com/photos/red-alphabet-decors-0sBTrm726C8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||||
|
|
||||||
I had the idea of maintaining my website in both English and Spanish; however, that is giving me some trouble, like taking more time to create content.
|
_Note: I retaked redacting in Spanish for my website, but I'll keep this post
|
||||||
|
anyway_
|
||||||
|
|
||||||
|
I had the idea of maintaining my website in both English and Spanish; however,
|
||||||
|
that is giving me some trouble, like taking more time to create content.
|
||||||
|
|
||||||
That's why I have decided to only create content in English for now and in the
|
That's why I have decided to only create content in English for now and in the
|
||||||
future.
|
future.
|
@ -38,7 +38,7 @@ new challenges.
|
|||||||
|
|
||||||
I want to start freelancing, I'll be creating templates and demo projects for
|
I want to start freelancing, I'll be creating templates and demo projects for
|
||||||
selling my services as a Frontend Developer mainly, but I'll keep learning about
|
selling my services as a Frontend Developer mainly, but I'll keep learning about
|
||||||
backend and cloud, as well as keep practicing my English to enter the USA or
|
Backend and Cloud, as well as keep practicing my English to enter the USA or
|
||||||
European markets.
|
European markets.
|
||||||
|
|
||||||
Maybe I'll not achieve this in 2024, but I must keep growing, as each year
|
Maybe I'll not achieve this in 2024, but I must keep growing, as each year
|
||||||
@ -73,7 +73,7 @@ as I already had a good habit of saving, but I want my money to keep growing
|
|||||||
more for a dignified retirement.
|
more for a dignified retirement.
|
||||||
|
|
||||||
I'm spending less time on social media,
|
I'm spending less time on social media,
|
||||||
[even I wrote an article about this](https://juancman.dev/en/blog/the-monotony-of-social-media),
|
[even I wrote an article about this](/blog/the-monotony-of-social-media),
|
||||||
doing things that **I really want to do** instead.
|
doing things that **I really want to do** instead.
|
||||||
|
|
||||||
Is really horrible how much time social media steals from us, keeping us away
|
Is really horrible how much time social media steals from us, keeping us away
|
@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: The reason to create a version 2.0 of my website
|
title: The Reason to Create a Version 2.0 of my Website
|
||||||
description: I commited some errors when creating the first version of my website, here I will share what I have learned.
|
description: I commited some errors when creating the first version of my website, here I will share what I have learned.
|
||||||
tags: [Tech]
|
tags: [Tech]
|
||||||
image: /blog/website-2.0/banner.png
|
image: /blog/website-2.0/banner.png
|
||||||
imageCaption: Tech Stack of this website. Next.js, Vercel, React.js, TypeScript and TailwindCSS
|
imageCaption: Tech Stack of this website. Next.js, Vercel, React.js, TypeScript and TailwindCSS
|
||||||
date: 2023-4-7
|
date: 2023-4-7
|
||||||
author: Juan Manzanero
|
author: Juan Manzanero
|
||||||
rss: true
|
rss: false
|
||||||
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
 _Tech Stack of this website.
|
 _Tech Stack of this website.
|
107
src/content/blog/es/como-funcionan-las-computadoras.mdx
Normal file
107
src/content/blog/es/como-funcionan-las-computadoras.mdx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Cómo Funcionan las Computadoras
|
||||||
|
description: Hoy en día usamos, de alguna manera, una computadora en casi toda actividad de nuestras vidas, volviéndose indispensables.
|
||||||
|
tags: [Tech, Informatic]
|
||||||
|
image: /blog/how-computers-works/banner.jpg
|
||||||
|
imageCaption: Una laptop abierta. Foto de Philipp Katzenberger en Unsplash
|
||||||
|
date: 2023-5-29
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Una laptop abierta. Foto de
|
||||||
|
[Philipp Katzenberger](https://unsplash.com/@fantasyflip?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText')
|
||||||
|
en
|
||||||
|
[Unsplash](https://unsplash.com/photos/iIJrUoeRoCQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)_
|
||||||
|
|
||||||
|
Hoy en día usamos, de alguna manera, una computadora en casi toda actividad de nuestras vidas, podría ser para trabajo o solo por diversión, pero si lo pensamos con cuidado, las computadoras son un invento del siglo pasado, y han cambiado nuestras vidas.
|
||||||
|
|
||||||
|
Nuevos empleos han aparecido, nuevas carreras por estudiar, nuevos problemas por resolver.
|
||||||
|
|
||||||
|
Pero, ¿realmente sabemos cómo las computadoras penetraron en nuestras vidas?, ¿realmente sabemos cómo funciona una computadora?, ¿cómo funciona Internet?
|
||||||
|
|
||||||
|
Hay muchas personas que usan sus smartphones para comunicarse con su familia y amigos, para compartir sus vidas, pero no saben cómo es esto posible.
|
||||||
|
|
||||||
|
No estoy diciendo que todos deberían tener una Ingeniería en Software o ser expertos en IT, pero entender nuestra actual línea temporal puede ser un conocimiento destacable a tener.
|
||||||
|
|
||||||
|
## El poder de las computadoras
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Las computadoras están hechas para expandir nuestros cerebros, desde cosas como enviar mensajes a personas del otro lado del planeta, hasta crear una app que agiliza un proceso de entrega.
|
||||||
|
|
||||||
|
Todo esto se hace tan solo “girando” unos y ceros (1´s y 0´s), pero, ¿cómo es esto posible?
|
||||||
|
|
||||||
|
Si ya has visto Él Código Enigma, ya sabes parte de esa historia.
|
||||||
|
|
||||||
|
### Fundamentos de Computación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Alan Turing fue el inventor de la Máquina de Turing, una simple pero poderosa máquina que puede recibir instrucciones para moverse en una larga cinta, cambiando el estado de cada ranura. Estas 3 cosas, una cabeza, una cinta larga y un conjunto de instrucciones son las bases para las computadoras modernas.
|
||||||
|
|
||||||
|
La cabeza es la Unidad de Procesamiento Central (CPU), una pieza de hardware que puede ser usada con propósitos generales, recibiendo instrucciones (Algoritmo) que son convertidos a pulsos eléctricos, entendiendo si la electricidad pasa o no, si es verdadero o falso, 1 ó 0. Todas estas instrucciones son guardadas en la Memoria Aleatoria de Acceso, Random Access Memory (RAM) para un acceso rápido del trabajo que se ha hecho. La Memoria de Solo Lectura, Read-Only Memory (ROM) es usada para guardar datos persistentes que se necesitan recuperar aunque la computadora se apague.
|
||||||
|
|
||||||
|
Un algoritmo puede ser visto como una receta de cocina, declaras los ingredientes (variables) y los pasos a seguir (funciones) para obtener un resultado.
|
||||||
|
|
||||||
|
Una variable es un identificador que apunta a un slot en la RAM, guardando un valor que puede ser un número, un texto (se le conoce como “string”), un booleano (true o false), un object (un conjunto de múltiples variables y funciones que puede ser instanciado), etc.
|
||||||
|
|
||||||
|
Las funciones son bloques de instrucciones que cumplen una tarea, como obtener la ubicación actual o enviar un mensaje.
|
||||||
|
|
||||||
|
Y te estarás preguntando, ¿cómo puedo decirle a una computadora lo que quiero hacer?
|
||||||
|
|
||||||
|
### Lenguajes de programación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Si intentas hablar con alguien que no habla el mismo idioma que tu, intentas usar un traductor o usas gestos, algo que sabes que ambos puedan de alguna manera entender, lo mismo ocurre con las computadoras.
|
||||||
|
|
||||||
|
Las computadoras son poderosas, pero necesitan a alguien que les diga qué hacer, este es el trabajo de los humanos, y para conseguirlo usamos lenguajes de programación. Con un lenguaje de programación usas una sintaxis específica para decirle a la computadora lo que quieres hacer, una vez hecho esto compilas el archivo que contiene las instrucciones que escribiste, entonces es transformado a un lenguaje que las computadoras pueden entender con mayor facilitad (1´s y 0´s) y entonces la computadora realiza la tarea.
|
||||||
|
|
||||||
|
Hay diferentes lenguajes de programación, y todos ellos están diseñados para solucionar necesidades específicas. Lenguajes como C y C++ son lenguajes de bajo nivel, esto quiere decir que son muy cercanos a cómo una computadora “habla” y son usados para controlar y administrar la memoria en apps de alto rendimiento, o para iluminar las pantallas de tu computadora.
|
||||||
|
|
||||||
|
Existe Java, un lenguaje que crea un entorno cuando es compilado, lo que le permite ser utilizado en casi cualquier computadora.
|
||||||
|
|
||||||
|
JavaScript (NO tiene que ver con Java o algo así) es un lenguaje que los navegadores entienden, gracias a JavaScript podemos acceder a una página web y ver asombrosas interacciones cuando le damos click a un botón, inicar sesión con un username y password, y más.
|
||||||
|
|
||||||
|
JavaScript es un lenguaje de alto nivel, es más fácil de aprender que Java o C, pero no por eso es mejor o peor, simplemente resuelve una necesidad diferente.
|
||||||
|
|
||||||
|
### El Navegador
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Es un poderoso software que puede acceder a otras computadoras usando el Hyper Text Transfer Protocol (HTTP), significa que gracias a este protocolo distintas computadoras pueden enviar y recibir información para comunicarse, incluso si están muy lejos. Los navegadores reciben datos en forma de archivos, principalmente tres:
|
||||||
|
|
||||||
|
- Hyper Text Markup Language (HTML)
|
||||||
|
- Cascading Styles Sheets (CSS)
|
||||||
|
- JavaScript
|
||||||
|
|
||||||
|
### HTML
|
||||||
|
|
||||||
|
Ayuda al navegador a estructurar los datos como textos o imágenes, usando un lenguaje de marcado (mediante tags). El navegador puede reconocer dónde hay un input para escribir nuestro email, o un botón para subscribirnos a nuestro artista favorito.
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
Ayuda a colorear y formar los tags de HTML usando selectores. Puede ser usado para cambiar el color de fondo de la página, darle a un botón bordes redondeados, cambiar el color de texto, todo lo que tu creatividad pueda imaginar.
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
Combinando HTML y CSS con Javascript se crea un sitio web interactivo, o aplicación web (como este). Puedes por ejemplo, añadir un botón que cambie el tema oscuro al claro, o guardar items en un carrito de compras y mostrar el número de items que tienes guardados.
|
||||||
|
|
||||||
|
Para todo esto necesitas guardar tus archivos en algún sitio, permitiéndole a las personas acceder a la computadora para descargar todos estos archivos usando sus navegadores, y para eso existen los servidores.
|
||||||
|
|
||||||
|
Los servidores son computadoras conectadas a Internet, guardan archivos que pueden ser descargados o subidos usando protocolos y reglas de seguridad. Algunas compañías como Google o Microsoft tienen múltiples centros con muchos servidores en diferentes regiones en el planeta, llamados Data Centers, y pueden ser usados por una tarifa para guardar tu web app, estos múltiples Data Centers se les conoce como Cloud.
|
||||||
|
|
||||||
|
### La Nube
|
||||||
|
|
||||||
|
Administrar una poderosa computadora puede ser difícil, pero si sabes cómo usarla, puedes ahorrarte mucho dinero en lugar de mantener servidores locales que necesiten estar encendidos 24/7. Gracias a la Cloud podemos entregar apps más rápidas, y podemos tener un servicio 24/7 para nuestros clientes con un costo marginal.
|
||||||
|
|
||||||
|
## Las Computadoras Cambiaron la Humanidad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Las computadoras simplifican nuestras tareas diarias, el software puede ser fácilmente replicado y distribuido sin la necesidad de logística como si lo necesita un producto tangible. Solo necesitas una conexión a Internet para acceder al proyecto de alguien.
|
||||||
|
|
||||||
|
No necesitas una fábrica o un recurso natural como madera para producir papel, necesitas un grupo de ingenieros, diseñadores UX/UI, digital marketers, y otras personas enfocadas en IT para alcanzar a miles de clientes.
|
||||||
|
|
||||||
|
La razón por la que las computadoras son tan poderosas es debido a que su costo marginal es mínimo, no necesitas extraer una materia prima de la tierra para construir una app, necesitas un grupo de personas talentosas que usen sus cerebros para crear soluciones.
|
89
src/content/blog/es/la-monotonia-de-las-redes-sociales.mdx
Normal file
89
src/content/blog/es/la-monotonia-de-las-redes-sociales.mdx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
title: La Monotonia de las Redes Sociales
|
||||||
|
description: La abstracción de la interacción humana mediante software ha causado muchos problemas que antes no existían.
|
||||||
|
tags: [Thoughts]
|
||||||
|
image: /blog/the-monotony-of-social-media/banner.jpg
|
||||||
|
imageCaption: Persona checando sus redes sociales. Foto de Austin Distel en Unsplash
|
||||||
|
date: 2023-7-17
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
_Persona checando sus redes sociales. Foto de
|
||||||
|
[Austin Distel](https://unsplash.com/@austindistel?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||||
|
en
|
||||||
|
[Unsplash](https://unsplash.com/photos/person-using-both-laptop-and-smartphone-tLZhFRLj6nY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||||
|
|
||||||
|
Es evidente que muchas interacciones en Internet ocurren a través de las redes sociales,
|
||||||
|
lo que te permite enviar solicitudes de amistad, chatear o compartir memes y fotos.
|
||||||
|
|
||||||
|
Sin embargo, la abstracción de la interacción humana mediante software ha causado muchos
|
||||||
|
problemas que antes no existían.
|
||||||
|
|
||||||
|
Cuando empezó Internet, muchas personas crearon sus propios sitios web porque eso era lo
|
||||||
|
que debías tener si querías ser popular, y muchos de estos sitios web eran simplemente como
|
||||||
|
blogs donde los usuarios compartían sus pasatiempos como películas, deportes, libros, videojuegos, etc.
|
||||||
|
|
||||||
|
Esta motivación llevó a la creación de sitios web únicos y personales, donde podías conocer
|
||||||
|
a alguien y sus gustos o disgustos.
|
||||||
|
|
||||||
|
Ahora, con el auge de las redes sociales a mediados de los años 2000, la gente prefiere conectarse
|
||||||
|
simplemente buscando un nombre o viendo los amigos de sus amigos, y enviar solicitudes de amistad
|
||||||
|
para intentar conectarse. Al principio, esto era genial, como cualquier cosa nueva, pero los problemas
|
||||||
|
comenzaron cuando empresas como Facebook (ahora Meta) o Google (con YouTube) necesitaban
|
||||||
|
monetizar sus plataformas, principalmente con anuncios.
|
||||||
|
|
||||||
|
Y, por supuesto, eso significa que tenían que suprimir, censurar o prohibir cualquier cosa
|
||||||
|
que pudiera ser perjudicial para la sociedad, como discursos de odio o retos estúpidos que pudieran
|
||||||
|
poner en peligro la vida de las personas.
|
||||||
|
|
||||||
|
Pero lo malo de esto es que homogeneizan a casi todos, obligándolos a actuar como los algoritmos
|
||||||
|
recomiendan usuarios con gustos y comentarios similares, guiando a las personas a actuar como alguien más,
|
||||||
|
y así sucesivamente.
|
||||||
|
|
||||||
|
Ahora casi todos hacen principalmente dos cosas: publicar fotos sobre sus vidas "perfectas" o compartir memes,
|
||||||
|
y no me malinterpretes, está bien entrar en las redes sociales y tratar de desconectar de tu trabajo o problemas,
|
||||||
|
pero usarlas todos los días como una vía de escape instantánea en lugar de enfrentar tus propios
|
||||||
|
problemas podría ser perjudicial a largo plazo, aislándote de la necesidad de socializar en la vida real,
|
||||||
|
con personas reales, y pensando que todos tienen una vida perfecta.
|
||||||
|
|
||||||
|
No, TODOS tienen problemas en sus vidas, incluso más que los tuyos, pero los algoritmos de las
|
||||||
|
redes sociales promueven principalmente "solo vibraciones positivas" y todas esas tonterías que en grandes
|
||||||
|
cantidades son perjudiciales para nuestras mentes.
|
||||||
|
|
||||||
|
Y no mencionemos la censura y el shadow-banning si publicas algo controvertido, puede ser algo que no se
|
||||||
|
debería tolerar, como incitar al odio a un grupo, o puede ser algo en lo que no todos estén de acuerdo,
|
||||||
|
pero podría ser útil reflexionar un poco al respecto, aunque sea controvertido.
|
||||||
|
|
||||||
|
¿Debería todo el mundo poder expresar lo que piensa? Sí, siempre y cuando no promueva el odio o
|
||||||
|
lastime a otras personas o animales.
|
||||||
|
|
||||||
|
Hay informes de que Twitter promueve el odio en el algoritmo, y Meta sabe que Instagram aumenta la
|
||||||
|
ansiedad y la depresión en los jóvenes, y de hecho, lo promueve... ya que todos los sentimientos
|
||||||
|
negativos te mantienen en las redes sociales interactuando con los demás, ya que eso es lo que esas
|
||||||
|
empresas venden, tus datos y tu tiempo a los anunciantes.
|
||||||
|
|
||||||
|
Recientemente escuché un video que hablaba sobre este tema, y sería genial si volviéramos a los
|
||||||
|
inicios de Internet, donde las personas creaban contenido como un pasatiempo, en lugar de buscar
|
||||||
|
validación a través de los me gusta y los comentarios, siendo personas más auténticas en lugar de productos.
|
||||||
|
|
||||||
|
¿Deberían las soluciones de software reemplazar las interacciones humanas? Creo que no, pero es
|
||||||
|
demasiado tarde para casi todos, pero si estás leyendo esto, comienza por cambiar tu vida primero si
|
||||||
|
quieres ser honesto contigo mismo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
###### Blog inspirado en "Why does every personal website look like this now?" by Eric Murphy en YouTube.
|
||||||
|
|
||||||
|
Video de origen (en Inglés):
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
width="560"
|
||||||
|
height="315"
|
||||||
|
src="https://www.youtube-nocookie.com/embed/_x6SCSz7g5I"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
67
src/content/blog/es/participe-en-una-hackaton.mdx
Normal file
67
src/content/blog/es/participe-en-una-hackaton.mdx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Participé en una Hackaton
|
||||||
|
description: Recientemente participé en un Hackathon de Supabase, formando un equipo con personas de otros países.
|
||||||
|
tags: [Tech, Hobby, Hackaton]
|
||||||
|
image: /blog/i-participated-in-a-hackathon/banner.png
|
||||||
|
imageCaption: "Tech stack utilizado: Supabase, Next.js y Shadcn/UI, ¡mi primera hackathon!"
|
||||||
|
date: 2023-8-16
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Tech stack used:
|
||||||
|
Supabase, Next.js and Shadcn/UI, My first hackathon!_
|
||||||
|
|
||||||
|
Recientemente participé en un [Hackathon de Supabase](https://supabase.com/blog/supabase-lw8-hackathon),
|
||||||
|
formando un equipo con personas de otros países, uno de Bangladesh y otro de Brasil,
|
||||||
|
utilizando inglés para comunicarnos en Discord.
|
||||||
|
|
||||||
|
La temática del Hackathon era libre, la única regla principal era utilizar Supabase para cualquier función,
|
||||||
|
como autenticación, como base de datos PostgreSQL o para utilizar vectores en la inteligencia artificial.
|
||||||
|
|
||||||
|
Tuvimos 10 días para construir un producto utilizando cualquier tecnología y subirlo a un repositorio en GitHub.
|
||||||
|
|
||||||
|
Desarrollamos una aplicación de comercio electrónico con un modelo de productos similar a Walmart,
|
||||||
|
implementando vectores para obtener mejores resultados en las búsquedas.
|
||||||
|
|
||||||
|
Utilizando Supabase, implementamos autenticación y rutas protegidas,
|
||||||
|
de manera que el usuario deba iniciar sesión para ver recomendaciones y más.
|
||||||
|
|
||||||
|
El usuario puede agregar productos al carrito de compras y revisar sus artículos para guardarlos y
|
||||||
|
ver recomendaciones, así como los artículos que se compran con frecuencia.
|
||||||
|
|
||||||
|
Mi papel principal se centró en crear la interfaz de usuario utilizando Next.js 13app router,
|
||||||
|
protegiendo las rutas solo para usuarios autenticados, y creando componentes reutilizables como
|
||||||
|
tarjetas de producto, y por supuesto, haciendo que el diseño sea adaptable tanto para dispositivos móviles
|
||||||
|
como para escritorio.
|
||||||
|
|
||||||
|
Utilizamos [Shadcn/UI](https://ui.shadcn.com/), ya que estos componentes ya implementan funcionalidades
|
||||||
|
con accesibilidad, como modales o barras laterales. Por ejemplo, la barra lateral que aparece cuando estás
|
||||||
|
en un dispositivo móvil y lo abres con el botón en el encabezado, con una animación suave.
|
||||||
|
|
||||||
|
Presentamos el proyecto a tiempo y estamos esperando los resultados.
|
||||||
|
|
||||||
|
Esta es la primera vez que participo en un Hackathon y realmente lo disfruté,
|
||||||
|
espero seguir contribuyendo al proyecto en GitHub.
|
||||||
|
|
||||||
|
Es increíble trabajar con personas de otros países, utilizando el inglés aunque no sea nuestra lengua materna,
|
||||||
|
pero con un propósito en común: crear un gran producto.
|
||||||
|
|
||||||
|
Seguiré buscando participar en más Hackathones en el futuro y contribuir a
|
||||||
|
proyectos de código abierto (open source) en Github, porque realmente disfruto la sensación de
|
||||||
|
desarrollar algo grande con más personas.
|
||||||
|
|
||||||
|
Aprendí mucho en estos pocos días, como la integración de Next.js con Supabase para autenticación y
|
||||||
|
rutas protegidas, utilizando la documentación de Supabase como guía, y utilizando por primera vez Shadcn/UI,
|
||||||
|
y estoy ansioso por seguir usándolo.
|
||||||
|
|
||||||
|
Me llevó mucho tiempo participar en un Hackathon, ya que antes dudaba de mi experiencia,
|
||||||
|
pero la realidad es que nunca estaremos listos para nuevos desafíos, porque si ya estás listo,
|
||||||
|
significa que es demasiado tarde.
|
||||||
|
|
||||||
|
Quiero aprender más sobre el uso de vectores para la inteligencia artificial,
|
||||||
|
así que investigaré más sobre el tema, ya que la tecnología tiende a avanzar en esa dirección.
|
||||||
|
|
||||||
|
Quién sabe cuál será la próxima gran tendencia tecnológica o cuándo llegará.
|
||||||
|
|
||||||
|
[Puedes ver el proyecto: Grocewise aquí](https://groce-wise.vercel.app/)
|
100
src/content/blog/es/rewind-2023-y-planes-futuros.mdx
Normal file
100
src/content/blog/es/rewind-2023-y-planes-futuros.mdx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: Rewind 2023 y Planes Futuros
|
||||||
|
description: Mi rewind del 2023 y mis planes para el 2024 y más allá.
|
||||||
|
tags: [Thoughts]
|
||||||
|
image: /blog/rewind-2023-and-future-plans/banner.jpg
|
||||||
|
imageCaption: Puesta de sol con un letrero, foto de Javier Allegue Barros en Unsplash
|
||||||
|
date: 2023-12-16
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Foto de
|
||||||
|
[Javier Allegue Barros](https://unsplash.com/@soymeraki?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||||
|
en
|
||||||
|
[Unsplash](https://unsplash.com/photos/silhouette-of-road-signage-during-golden-hour-C7B-ExXpOIE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||||
|
|
||||||
|
Mi rewind del 2023 y mis planes para el 2024 y más allá.
|
||||||
|
|
||||||
|
Espero que la estés pasando bien en estas fechas.
|
||||||
|
|
||||||
|
La vida es una sucesión de decisiones, y en retrospectiva, estoy feliz de que este año haya hecho
|
||||||
|
las correctas.
|
||||||
|
|
||||||
|
## En retrospectiva sobre mi carrera este 2023
|
||||||
|
|
||||||
|
Crecí drásticamente como Full Stack Developer, aprendiendo nuevas libraries y estableciendo mi tech
|
||||||
|
stack.
|
||||||
|
|
||||||
|
Incluso comencé con hacking (no lo que sueles escuchar en las noticias), creando side projects
|
||||||
|
buscando crear una solución.
|
||||||
|
|
||||||
|
Actualicé este sitio, creando nuevas funcionalidades para crear contenido.
|
||||||
|
|
||||||
|
Ahora tengo más confianza en mis habilidades, listo para seguir creciendo y tomando nuevos desafios.
|
||||||
|
|
||||||
|
## Planes futuros para mi carrera este 2024
|
||||||
|
|
||||||
|
Quiero comenzar a hacer freelancing, estaré creando templates y proyectos demo para vender mis
|
||||||
|
servicios como Frontend Developer principalmente, pero seguiré aprendiendo sobre Backend y Cloud,
|
||||||
|
así como seguiré practicando mi Inglés para entrar al mercado estadounidense o europeo.
|
||||||
|
|
||||||
|
Quizás no lo consiga este 2024, pero debo seguir creciendo, ya que cada año que pasa, estaré más
|
||||||
|
preparado.
|
||||||
|
|
||||||
|
Veré si podría contribuir a algún proyecto Open Source, ya que casí todas las herramientas que uso
|
||||||
|
son Open Source, y no podría estar aquí sino fuera por la ayuda de proyectos Open Source.
|
||||||
|
|
||||||
|
Honestamente, mi verdadero deseo es trabajar tiempo completo en una startup Software as a Service,
|
||||||
|
o cualquier startup que se enfoque en Software.
|
||||||
|
|
||||||
|
Lo bueno es que continuo creciendo profesionalmente, y espero (y lo haré) que el siguiente año
|
||||||
|
alcance más metas.
|
||||||
|
|
||||||
|
Y por supuesto, continuaré con el hacking (de forma creativa, no robando información u otras cosas criminales)
|
||||||
|
con side projects, crear una fuente extra de ingresos sería genial para mis finanzas.
|
||||||
|
|
||||||
|
## Retrospectiva personal del 2023 y más
|
||||||
|
|
||||||
|
Este año redescubrí el hobby de leer, y realmente lo disfruto.
|
||||||
|
|
||||||
|
Descubrí mi nuevo libro favorito, **Ready Player One**, fue una lectura emocionante, el siguiente año
|
||||||
|
leeré la secuela.
|
||||||
|
|
||||||
|
También comencé a leer **Ikigai**, para seguir adquiriendo buenos hábitos para una vida larga y feliz,
|
||||||
|
así como buscar un propósito en la vida.
|
||||||
|
|
||||||
|
Otro libro que comencé a leer es **The Little Book of Common Sense Investing**, ya que teniendo el buen
|
||||||
|
hábito de ahorrar, pero quiero que mi dinero siga creciendo más para un retiro digno.
|
||||||
|
|
||||||
|
Estoy gastando menos tiempo en redes sociales,
|
||||||
|
[incluso escribí un artículo sobre esto](/es/blog/la-monotonia-de-las-redes-sociales),
|
||||||
|
haciendo cosas que **realmente quiero hacer** en su lugar.
|
||||||
|
|
||||||
|
Es realmente horrible cuanto tiempo nos roban las redes sociales, manteniéndonos alejados de hacer cosas
|
||||||
|
que realmente disfrutamos.
|
||||||
|
|
||||||
|
Estoy haciendo ejercicio moderado, pero dentro de casa, quiero ir afuera también, necesito más luz solar.
|
||||||
|
|
||||||
|
Estoy feliz de pasar tiempo con mi familia, incluso si la mayoría del tiempo estoy trabajando o estudiando,
|
||||||
|
mantengo contacto con mis seres queridos, y continuaré haciéndolo, desde luego.
|
||||||
|
|
||||||
|
## Planes personales futuros para 2024 y más
|
||||||
|
|
||||||
|
Seguiré leyendo, escribiré más para este website.
|
||||||
|
|
||||||
|
Quiero adquirir nuevos hábitos, como pixel art.
|
||||||
|
|
||||||
|
Retomaré un viejo hobby, **GameDev**.
|
||||||
|
|
||||||
|
Como Unity ha estado haciendo cosas cuestionables, usaré [Godot](https://godotengine.org/) en su lugar.
|
||||||
|
|
||||||
|
No tengo en mente algún proyecto grande o algo así, solo retomar el desarrollar demos sencillas de videojuegos,
|
||||||
|
estaría genial lanzar un videojuego pequeño, pero terminado.
|
||||||
|
|
||||||
|
Créeme, es muy, **MUY** difícil desarrollar videojuegos, así que en lugar de abrumarme, mantendré mis
|
||||||
|
**ambiciones simples**, pero **constantes**.
|
||||||
|
|
||||||
|
### ¡Felices fiestas!
|
||||||
|
|
||||||
|

|
114
src/content/blog/es/una-mejor-forma-de-consumir-contenido.mdx
Normal file
114
src/content/blog/es/una-mejor-forma-de-consumir-contenido.mdx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
title: Una Mejor Forma de Consumir Contenido
|
||||||
|
description: Obtén tus noticias sin visitar websites con algoritmos que muestran contenido que no quieres ver.
|
||||||
|
tags: [Tech]
|
||||||
|
image: /blog/a-better-way-for-consuming-content/banner.webp
|
||||||
|
imageCaption: Periódicos. Foto de Ashni en Unsplash
|
||||||
|
date: 2024-4-11
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Foto de
|
||||||
|
[Ashni](https://unsplash.com/@ashni_ahlawat?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||||
|
en
|
||||||
|
[Unsplash](https://unsplash.com/photos/text-ePWaAwUn80k?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||||
|
|
||||||
|
Obtén tus noticias sin visitar websites con algoritmos que muestran contenido que no quieres ver.
|
||||||
|
|
||||||
|
## Algoritmos que dictan lo que ves
|
||||||
|
|
||||||
|
Las redes sociales no están diseñadas para mostrarte las noticias más novedosas e importantes,
|
||||||
|
sino para mostrarte contenido dictado por un algoritmo.
|
||||||
|
|
||||||
|
Y este contenido es normalmente, viral; y viral no quiere decir interesante.
|
||||||
|
|
||||||
|
Normalmente estos algoritmos priorizan contenido que te hacen enojar.
|
||||||
|
|
||||||
|
Contenido que promueve la negatividad obtiene más "clicks" que aquel que promueve la positividad.
|
||||||
|
|
||||||
|
Esa es la razón por la cual Twitter y Facebook están llenos de posts estúpidos e irrelevantes (por lo regular).
|
||||||
|
|
||||||
|
Por supuesto, es genial cuando el algoritmo te muestra contenido que te gusta,
|
||||||
|
descubriendo nuevas personas y páginas, pero eso no es lo usual.
|
||||||
|
|
||||||
|
Sin mencionar los molestos anuncios y más cosas que quieren que les hagas click.
|
||||||
|
|
||||||
|
Meta (anteriormente Facebook) sabe sobre esto, y promueve en sus productos como Instagram y Facebook,
|
||||||
|
lo mismo con Twitter.
|
||||||
|
|
||||||
|
## La Solución: News Aggregators (RSS)
|
||||||
|
|
||||||
|
RSS es un acrónimo de "Really Simple Syndication".
|
||||||
|
|
||||||
|
Es una tecnología antigua, no muy promovida por las compañías.
|
||||||
|
|
||||||
|
Esto es debido a que cuando lees un post en un RSS Reader, no debes de visitar el website,
|
||||||
|
y el website no puede mostrarte anuncios usando Google Ads (por ejemplo). No generas tráfico,
|
||||||
|
tus visitas no cuentan, al menos no si no abres el link del post en tu RSS Reader.
|
||||||
|
|
||||||
|
Lo bueno que casi cualquier RSS Reader te muestra el contenido organizado por fechas,
|
||||||
|
no por un algoritmo raro que quiere hacerte enojar.
|
||||||
|
|
||||||
|
Para mi website utilizo un script en Node.js que toma todos los archivos `.mdx` dentro de `content/blog`
|
||||||
|
y `content/portfolio`, entonces genera los RSS Items, aquellos con `rss: true` en sus metadatos.
|
||||||
|
|
||||||
|
## Cómo usar un RSS Reader
|
||||||
|
|
||||||
|
Primero, debes descargar uno.
|
||||||
|
|
||||||
|
Hay muchas opciones:
|
||||||
|
|
||||||
|
- [NetNewsWire](https://netnewswire.com/): un RSS Reader nativo para macOS y iOS, gratis y Open Source,
|
||||||
|
mi opción favorita como ~~Pecador~~ usuario de Apple
|
||||||
|
- [Akregator](https://apps.kde.org/akregator): del proyecto KDE para Linux
|
||||||
|
- [Feeder](https://play.google.com/store/apps/details?id=com.nononsenseapps.feeder.play): para Android
|
||||||
|
- [Raven Reader](https://ravenreader.app/): aplicación de escritorio multiplataforma
|
||||||
|
|
||||||
|
### Agregando Feeds
|
||||||
|
|
||||||
|
Ahora debes buscar por el link RSS de tu website favorito, ¡[como este](https://juancman.dev/es/rss.xml)!
|
||||||
|
|
||||||
|
```
|
||||||
|
https://juancman.dev/es/rss.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Si lo abres, verás una página rara con código similar a HTML.
|
||||||
|
|
||||||
|
Una vez copiado, ve a tu RSS app y busca "Agregar feed" o algo similar, y pega el link, ¡y listo!,
|
||||||
|
ahora obtendrás los últimos posts de mi website.
|
||||||
|
|
||||||
|
### Agregando Feeds de Redes Sociales
|
||||||
|
|
||||||
|
Puedes agregar incluso feeds de sitios como Reddit o YouTube.
|
||||||
|
|
||||||
|
#### Reddit
|
||||||
|
|
||||||
|
Solo cambia `[SUBREDDIT]` por el nombre del subreddit a añadir:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://reddit.com/r/[SUBREDDIT]/new/.rss
|
||||||
|
```
|
||||||
|
|
||||||
|
#### YouTube
|
||||||
|
|
||||||
|
Ve a el canal por añadir, luego ve a la pestaña "Acerca de", da click en **Compartir > Copiar ID del Canal**.
|
||||||
|
|
||||||
|
Ahora solo cambia `[CHANNEL ID]` por el copiado:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://youtube.com/feeds/videos.xml?channel_id=[CHANNEL ID]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mis Feeds Favoritos
|
||||||
|
|
||||||
|
- [juancman.dev (¡obviamente!)](https://www.juancman.dev/es/rss.xml)
|
||||||
|
- [Astronomic Picture of the Day (apod)](https://apod.com/feed.rss)
|
||||||
|
- [Earth Science Picture of the Day (epod)](https://feeds2.feedburner.com/EarthSciencePictureoftheDay)
|
||||||
|
- [Erick Murphy](https://ericmurphy.xyz/index.xml)
|
||||||
|
- [Luke Smith](https://lukesmith.xyz/index.xml)
|
||||||
|
|
||||||
|
## Más Sobre RSS
|
||||||
|
|
||||||
|
- [Privacy Tools - RSS Feed Readers](https://www.privacytools.io/privacy-rss-feed-readers)
|
||||||
|
- [Privacy Guides - News Aggregators](https://www.privacyguides.org/en/news-aggregators/)
|
64
src/content/blog/es/website-2.0.mdx
Normal file
64
src/content/blog/es/website-2.0.mdx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: La Razón para Crear una Versión 2.0 de mi Website
|
||||||
|
description: Cometí algunos errores al crear la primera versión de mi sitio web, aquí compartiré lo que he aprendido.
|
||||||
|
tags: [Tech]
|
||||||
|
image: /blog/website-2.0/banner.png
|
||||||
|
imageCaption: Tech Stack usado para este sitio web. Next.js, Vercel, React.js, TypeScript and TailwindCSS
|
||||||
|
date: 2023-4-7
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: false
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Tech Stack of this website.
|
||||||
|
Next.js, Vercel, React.js, TypeScript and TailwindCSS_
|
||||||
|
|
||||||
|
La primera versión de mi sitio web fue uno de mis mayores proyectos hasta ahora, pero ahora que tengo más experiencia como ingeniero Frontend, me doy cuenta de que no hice suficiente investigación para crear un sitio web con un blog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Renderización del lado del cliente (CSR) vs. Renderización del lado del servidor (SSR) vs. Generación estática (SG)
|
||||||
|
|
||||||
|
Al **desarrollar** una aplicación **web**, debes **pensar** en el tipo de **renderizado** que se debe utilizar, teniendo en cuenta los **requisitos** de los **problemas** que deseas **resolver**.
|
||||||
|
|
||||||
|
### Generación del lado del cliente (CSG)
|
||||||
|
|
||||||
|
Por ejemplo, una aplicación **web** como una **SaaS** para crear tareas y administrar personas tendrá **páginas dinámicas** para mostrar las tareas, actualizar las tarjetas cuando se editan o eliminan, mostrar notificaciones, etc. En esta situación, un **CSR** sería **mejor** para **renderizar** la página **cada** vez que el usuario **solicita** acceso para **mantener** los datos **actualizados**. Sin embargo, un **CSR** necesita **hidratar** la página cuando se **solicita**, lo que provoca una **carga lenta** al **principio** y utiliza **más recursos** de la PC del usuario.
|
||||||
|
|
||||||
|
### Renderización del lado del servidor (SSR)
|
||||||
|
|
||||||
|
Esto podría resolverse utilizando **SSR**, que consiste en **generar** la página en el **servidor** donde se aloja la aplicación web utilizando toda la **potencia** que un **servidor** puede proporcionar. El problema es que se requiere un **servidor**. Google Cloud proporciona opciones sin servidor como App Engine o Cloud Run, pero deberás aprender sobre estos servicios y cómo implementar el
|
||||||
|
proyecto, por lo que el **conocimiento técnico** es **alto**.
|
||||||
|
|
||||||
|
La **desventaja** de **CSG** y **SSR** es que, debido a que **cada página** debe ser **renderizada** en cada **solicitud**, los **rastreadores web** y los **motores de búsqueda** como Google tardarán más tiempo en obtener información sobre tu página, lo que resultará en una **prioridad SEO** baja.
|
||||||
|
|
||||||
|
### Generación estática (SG)
|
||||||
|
|
||||||
|
Bueno, si una **página** no requiere **buscar** datos para **cada solicitud**, entonces puedes usar **SG**, lo que significa que la **página** se **genera** cuando construyes el directorio de producción **antes** de implementarlo. La página se generará en HTML/CSS/JS una vez, y si necesitas **actualizar** los datos en esa página, deberás hacer los cambios y **implementar** el proyecto. Sí, deberás ser más cuidadoso al revisar los cambios antes de implementarlos, pero como la página ya está generada, los **rastreadores web** y el **motor de búsqueda** de Google obtendrán la **información** de tu página **más rápido**, lo que **mejorará** tu **SEO**.
|
||||||
|
|
||||||
|
## Lo interesante de Next.js
|
||||||
|
|
||||||
|
En el **pasado**, se requería **pensar** si usar CSG completo, SSR o SG, vinculando tu sitio web a sus respectivas secciones, como la aplicación, el blog, etc.
|
||||||
|
**[Next.js](https://nextjs.org/)** es un **meta-framework** de **Node.js** que utiliza **[React.js](https://react.dev/)** para construir la interfaz de usuario, y proporciona CSR, SSR, SG y más, por lo que puedes generar SG obteniendo datos de forma asíncrona, lo que te permite no tener que crear cada página estática.
|
||||||
|
|
||||||
|
Este es el **enfoque** utilizado para este sitio web, en lugar de obtener los datos en cada solicitud, solo obtengo los datos cuando creo la construcción del proyecto.
|
||||||
|
|
||||||
|
Cada artículo es una página SG, pero utilizo una **plantilla** para mantener todos los blogs similares, utilizando la sintaxis **markdown** para el contenido del blog, y con una **extensión** de **TailwindCSS** mantengo los estilos consistentes.
|
||||||
|
|
||||||
|
Así que, Next.js te permite elegir el método de renderizado para cada página de tu sitio web, esta característica permite crear sitios web increíbles en el mismo proyecto, manteniendo la coherencia y con tiempos de carga rápidos, Next.js incluso carga de forma perezosa cada página y comienza a cargarse cuando pasas el cursor sobre un enlace como Inicio, Contacto, etc.
|
||||||
|
|
||||||
|
> ¡Explicaré en más detalle la arquitectura de este proyecto en el futuro!
|
||||||
|
|
||||||
|
## **TailwindCSS vs MUI**
|
||||||
|
|
||||||
|
Elegí usar **[TailwindCSS](https://tailwindcss.com/)** para aprender sobre esta biblioteca CSS, y estoy impresionado por lo **rápido** que hace el desarrollo de los estilos de un proyecto web. **MUI** proporciona **funcionalidades**, pero a veces **presenta problemas** con la **hidratación**, como en mi sitio web anterior, cuando **cargas** la página por primera vez, tarda un **tiempo** en **mostrar** todos los **estilos**, ahora ya no ocurre porque TailwindCSS es CSS puro y las páginas son estáticas.
|
||||||
|
|
||||||
|
## Despliegue en Vercel
|
||||||
|
|
||||||
|
**[Vercel](https://vercel.com/)** es la empresa detrás de Next.js, y ofrece servicios de **hosting** optimizados para aplicaciones **Node.js**, y como estoy aprendiendo sobre desarrollo en la nube, tal vez podría intentar alojar esta web en un servicio en la nube como Cloud Run, pero esta vez elijo utilizar **Vercel** para obtener las **analíticas** que son muy útiles, y como el **plan hobby** me ofrece alojamiento gratuito para proyectos pequeños.
|
||||||
|
|
||||||
|
Para implementar, utilizo la **[Vercel CLI](https://vercel.com/docs/cli)**, bastante simple y directo.
|
||||||
|
|
||||||
|
## ¡Más contenido en camino!
|
||||||
|
|
||||||
|
Seguiré actualizando con publicaciones, características y más contenido para compartir mi experiencia, y ahora que estoy escribiendo este párrafo, creo que la próxima característica podría ser un boletín para notificar a las personas cuando creo una nueva publicación. ¡Hora de trabajar!
|
@ -30,8 +30,14 @@ const pages = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const videos = defineCollection({
|
||||||
|
type: "content",
|
||||||
|
schema: contentSchema
|
||||||
|
})
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
blog,
|
blog,
|
||||||
portfolio,
|
portfolio,
|
||||||
pages,
|
pages,
|
||||||
|
videos
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: About
|
title: About
|
||||||
description: This website was first created as a portfolio, but learning about how the personal website is the digital form of the house tree, I like the idea of going that way instead of a generic landing with my social media.
|
description:
|
||||||
|
This website was first created as a portfolio, but learning about how the
|
||||||
|
personal website is the digital form of the house tree, I like the idea of
|
||||||
|
going that way instead of a generic landing with my social media.
|
||||||
---
|
---
|
||||||
|
|
||||||
# About
|
# About
|
||||||
@ -9,11 +12,8 @@ This website was first created as a portfolio, but learning about how the
|
|||||||
personal website is the digital form of the house tree, I like the idea of going
|
personal website is the digital form of the house tree, I like the idea of going
|
||||||
that way instead of a generic landing with my social media.
|
that way instead of a generic landing with my social media.
|
||||||
|
|
||||||
I'm trying to expand my skills, as I'm a Frontend Developer (with knowledge on
|
This website is in English to reach more people; and in Spanish, because is my
|
||||||
Backend), but skills like writing are important.
|
mother tongue.
|
||||||
|
|
||||||
This website is in English to reach more people and put it into practice, but
|
|
||||||
Spanish is my mother tongue.
|
|
||||||
|
|
||||||
All content written here is without AI; I don't use it for generating ideas; the
|
All content written here is without AI; I don't use it for generating ideas; the
|
||||||
only exception is [LanguageTool](https://languagetool.org/) for validating my
|
only exception is [LanguageTool](https://languagetool.org/) for validating my
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: Contact
|
title: Contact
|
||||||
description: You can contact me if you want me to work, or just say hello.
|
description:
|
||||||
|
You can contact me if you want to say hi, contract me or ask me about a tool
|
||||||
|
that I use or some topic.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Contact
|
# Contact
|
||||||
|
|
||||||
You can contact me if:
|
You can contact me if:
|
||||||
|
|
||||||
- You want me to work
|
- Just say hi
|
||||||
- Just say hello
|
- You want to contract me
|
||||||
|
- Ask me about a tool that I use or some topic
|
||||||
|
|
||||||
Please consider that **I don't**:
|
Please, consider that:
|
||||||
|
|
||||||
- Work for free
|
- I won't work for free
|
||||||
- Work on your startup idea and just get equity in return (I can't pay my bills
|
- I won't work on your startup idea, and I'll just get equity in return (I can't
|
||||||
with lottery tickets)
|
pay my bills with lottery tickets)
|
||||||
- Work for you and get "exposure" (I can't pay my bills with exposure)
|
- I won't work for you and get "exposure" (I can't pay my bills with exposure)
|
||||||
- Communicate via phone number; all communication must be via email (we can use
|
- I won't communicate via phone number; all communication must be via email (we
|
||||||
Discord, Slack, etc. once you hire me).
|
can use Discord, Slack, etc. once you hire me).
|
||||||
|
|
||||||
## My email
|
## My email
|
||||||
|
|
||||||
|
22
src/content/pages/es/acerca-de.mdx
Normal file
22
src/content/pages/es/acerca-de.mdx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Acerca de
|
||||||
|
description:
|
||||||
|
Este website fue creado en un principio como portfolio, pero luego de aprender
|
||||||
|
como los websites personales son la forma difgital de la casa del árbol, me
|
||||||
|
gustó más esa idea en lugar de una página genérica con mis redes sociales.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Acerca de
|
||||||
|
|
||||||
|
Este website fue creado en un principio como portfolio, pero luego de aprender
|
||||||
|
como los websites personales son la forma difgital de la casa del árbol, me
|
||||||
|
gustó más esa idea en lugar de una página genérica con mis redes sociales.
|
||||||
|
|
||||||
|
Este website está en Inglés para llegar a más gente; y en Español, ya que es mi
|
||||||
|
lengua materna.
|
||||||
|
|
||||||
|
Todo el contenido escrito aquí es sin AI; no la uso para generar ideas; la única
|
||||||
|
excepción es [LanguageTool](https://languagetool.org/) para validar mi
|
||||||
|
gramática.
|
||||||
|
|
||||||
|
[](https://notbyai.fyi/)
|
40
src/content/pages/es/contacto.mdx
Normal file
40
src/content/pages/es/contacto.mdx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Contacto
|
||||||
|
description:
|
||||||
|
Puedes contactarme para decirme hola, contratarme o preguntarme sobre alguna
|
||||||
|
herramienta que uso o algún tema.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contacto
|
||||||
|
|
||||||
|
Puedes contactarme si:
|
||||||
|
|
||||||
|
- Solo quieres decirme "hola"
|
||||||
|
- Quieres contratarme
|
||||||
|
- Preguntarme sobre alguna herramienta que utilizo o sobre algún tema
|
||||||
|
|
||||||
|
Por favor, considera que:
|
||||||
|
|
||||||
|
- No trabajo gratis
|
||||||
|
- No trabajaré en tu idea de startup a cambio de equity (no puedo pagar mis
|
||||||
|
facturas con tickets de lotería)
|
||||||
|
- No trabajaré a cambio de "exposición" (no puedo pagar mis facturas con
|
||||||
|
exposición)
|
||||||
|
- No me comunicaré via número telefónico, toda comunicación debe ser via email
|
||||||
|
(podemos usar Discord, Slack, etc. una vez me contrates)
|
||||||
|
|
||||||
|
## Mi email
|
||||||
|
|
||||||
|
Solo cambia `[at]` por `@` y `[dot]` por `.`. Esto es para evitar que web
|
||||||
|
crawlers obtengan mi email.
|
||||||
|
|
||||||
|
```
|
||||||
|
contact[at]juancman[dot]dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Redes sociales
|
||||||
|
|
||||||
|
Puedes enviarme un mensaje en:
|
||||||
|
|
||||||
|
- [LinkedIn](https://www.linkedin.com/in/juancmandev)
|
||||||
|
- [GitHub](https://github.com/juancmandev)
|
61
src/content/pages/es/recursos.mdx
Normal file
61
src/content/pages/es/recursos.mdx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Resources
|
||||||
|
description:
|
||||||
|
Here you can find websites, YouTube channels, courses and more stuff that I
|
||||||
|
consume or find interesting.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Recursos
|
||||||
|
|
||||||
|
Here you can find **websites**, **YouTube channels**, **courses** and **more**
|
||||||
|
stuff that I consume or find interesting.
|
||||||
|
|
||||||
|
## Courses and Documentation
|
||||||
|
|
||||||
|
- [fireship.io](https://fireship.io)
|
||||||
|
|
||||||
|
- [MDN Web Docs](https://developer.mozilla.org/en-US)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- [Astro](https://astro.build/) - Tool for building websites, that's how I built
|
||||||
|
this one, really useful when you want a static website, but you can do Server
|
||||||
|
Side Rendering too
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org) - Dynamic and flexible React meta-framework,
|
||||||
|
previously used on this Website
|
||||||
|
|
||||||
|
- [PocketBase](https://pocketbase.io/) - Fast and light database.
|
||||||
|
|
||||||
|
- [Supabase](https://supabase.com) - Open Source Backend as a Service
|
||||||
|
alternative for Firebase, uses PostgreSQL and is really good if you're working
|
||||||
|
alone or you want a solid backend without investing to much
|
||||||
|
|
||||||
|
- [TailwindCSS](https://tailwindcss.com) - Best way to write CSS
|
||||||
|
|
||||||
|
- [shadcn/ui](https://ui.shadcn.com) - Best components for React, sinergy with
|
||||||
|
TailwindCSS
|
||||||
|
|
||||||
|
## YouTube channels
|
||||||
|
|
||||||
|
- [Mental Outlaw](https://www.youtube.com/channel/UC7YOGHUfC1Tb6E4pudI9STA)
|
||||||
|
|
||||||
|
- [Eric Murphy](https://www.youtube.com/channel/UC5KDiSAFxrDWhmysBcNqtMA)
|
||||||
|
|
||||||
|
- [Luke Smith](https://www.youtube.com/channel/UC2eYFnH61tmytImy1mTYvhA)
|
||||||
|
|
||||||
|
## Personal Websites
|
||||||
|
|
||||||
|
- [Eric Murphy](https://ericmurphy.xyz)
|
||||||
|
|
||||||
|
- [Luke Smith](https://lukesmith.xyz/)
|
||||||
|
|
||||||
|
## Favorite Blogs
|
||||||
|
|
||||||
|
- [Why I Will Never Join Mastodon (or the rest of the Fediverse)](https://ericmurphy.xyz/blog/mastodon)
|
||||||
|
|
||||||
|
- [Create More, Consume Less](https://www.bikobatanari.art/posts/2020/create-more) -
|
||||||
|
_Currently offline_
|
||||||
|
|
||||||
|
- [My Website is a Personal Museum](https://www.bikobatanari.art/posts/2020/personal-museum) -
|
||||||
|
_Currently offline_
|
@ -1,30 +1,31 @@
|
|||||||
---
|
---
|
||||||
title: Resources
|
title: Resources
|
||||||
description: Here you can find websites, YouTube channels, courses and more stuff that I consume or find interesting.
|
description:
|
||||||
|
Here you can find websites, YouTube channels, courses and more stuff that I
|
||||||
|
consume or find interesting.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
|
|
||||||
Here you can find websites, YouTube channels, courses and more stuff that I
|
Here you can find **websites**, **YouTube channels**, **courses** and **more**
|
||||||
consume or find interesting.
|
stuff that I consume or find interesting.
|
||||||
|
|
||||||
## Programming and Web Development
|
## Courses and Documentation
|
||||||
|
|
||||||
To **power-up** my career.
|
- [fireship.io](https://fireship.io)
|
||||||
|
|
||||||
### Websites (courses, docs, etc.)
|
- [MDN Web Docs](https://developer.mozilla.org/en-US)
|
||||||
|
|
||||||
- [fireship.io](https://fireship.io) - My favorite premium courses about WebDev
|
## Tech Stack
|
||||||
|
|
||||||
- [MDN Web Docs](https://developer.mozilla.org/en-US) - Best docs for HTML, CSS
|
- [Astro](https://astro.build/) - Tool for building websites, that's how I built
|
||||||
and JS
|
this one, really useful when you want a static website, but you can do Server
|
||||||
|
Side Rendering too
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
Technologies that I use for personal projects and sometimes for work.
|
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org) - Dynamic and flexible React meta-framework,
|
- [Next.js](https://nextjs.org) - Dynamic and flexible React meta-framework,
|
||||||
used on this Website
|
previously used on this Website
|
||||||
|
|
||||||
|
- [PocketBase](https://pocketbase.io/) - Fast and light database.
|
||||||
|
|
||||||
- [Supabase](https://supabase.com) - Open Source Backend as a Service
|
- [Supabase](https://supabase.com) - Open Source Backend as a Service
|
||||||
alternative for Firebase, uses PostgreSQL and is really good if you're working
|
alternative for Firebase, uses PostgreSQL and is really good if you're working
|
||||||
@ -35,34 +36,26 @@ Technologies that I use for personal projects and sometimes for work.
|
|||||||
- [shadcn/ui](https://ui.shadcn.com) - Best components for React, sinergy with
|
- [shadcn/ui](https://ui.shadcn.com) - Best components for React, sinergy with
|
||||||
TailwindCSS
|
TailwindCSS
|
||||||
|
|
||||||
|
## YouTube channels
|
||||||
|
|
||||||
### YouTube channels
|
- [Mental Outlaw](https://www.youtube.com/channel/UC7YOGHUfC1Tb6E4pudI9STA)
|
||||||
|
|
||||||
I mostly use YouTube as social media, I think there is still good content.
|
- [Eric Murphy](https://www.youtube.com/channel/UC5KDiSAFxrDWhmysBcNqtMA)
|
||||||
|
|
||||||
- [Mental Outlaw](https://www.youtube.com/channel/UC7YOGHUfC1Tb6E4pudI9STA) - Cool guy who talks about Linux, cybersecurity, privacy, and more.
|
- [Luke Smith](https://www.youtube.com/channel/UC2eYFnH61tmytImy1mTYvhA)
|
||||||
|
|
||||||
- [Eric Murphy](https://www.youtube.com/channel/UC5KDiSAFxrDWhmysBcNqtMA) - He uploads less videos but I really like his work too.
|
## Personal Websites
|
||||||
|
|
||||||
---
|
- [Eric Murphy](https://ericmurphy.xyz)
|
||||||
|
|
||||||
## Inspiration and Learning
|
- [Luke Smith](https://lukesmith.xyz/)
|
||||||
|
|
||||||
For writing, thinking or growing my career.
|
## Favorite Blogs
|
||||||
|
|
||||||
### Personal Websites
|
- [Why I Will Never Join Mastodon (or the rest of the Fediverse)](https://ericmurphy.xyz/blog/mastodon)
|
||||||
|
|
||||||
- [Eric Murphy](https://ericmurphy.xyz) - The guy who inspired me to retake this
|
|
||||||
side project and create more content
|
|
||||||
|
|
||||||
### Favorite Blogs
|
|
||||||
|
|
||||||
- [Why I Will Never Join Mastodon (or the rest of the Fediverse)](https://ericmurphy.xyz/blog/mastodon) -
|
|
||||||
Social media is bullshit
|
|
||||||
|
|
||||||
- [Create More, Consume Less](https://www.bikobatanari.art/posts/2020/create-more) -
|
- [Create More, Consume Less](https://www.bikobatanari.art/posts/2020/create-more) -
|
||||||
Today people prefers talk about celebrities and other peoples lifes rather
|
_Currently offline_
|
||||||
than our own milestones
|
|
||||||
|
|
||||||
- [My Website is a Personal Museum](https://www.bikobatanari.art/posts/2020/personal-museum) -
|
- [My Website is a Personal Museum](https://www.bikobatanari.art/posts/2020/personal-museum) -
|
||||||
The digital form of the three house
|
_Currently offline_
|
||||||
|
@ -112,7 +112,7 @@ export async function generateStaticParams(
|
|||||||
|
|
||||||
This will create each static page for each blog post.
|
This will create each static page for each blog post.
|
||||||
|
|
||||||
You can get the metadata of the .mdx file too.
|
You can get the metadata of the `.mdx` file too.
|
||||||
|
|
||||||
```tsx title="src/app/[locale]/blog/[slug]/page.tsx"
|
```tsx title="src/app/[locale]/blog/[slug]/page.tsx"
|
||||||
//...
|
//...
|
@ -2,7 +2,7 @@
|
|||||||
title: Peddler App
|
title: Peddler App
|
||||||
description: You hear the ice cream man in his truck, you try to catch him, but the guy is already far away.
|
description: You hear the ice cream man in his truck, you try to catch him, but the guy is already far away.
|
||||||
tags: [Tech, SideProject]
|
tags: [Tech, SideProject]
|
||||||
image: '/blog/peddler-app/banner.png'
|
image: "/blog/peddler-app/banner.png"
|
||||||
imageCaption: Peddler App provisional logo
|
imageCaption: Peddler App provisional logo
|
||||||
date: 2023-12-11
|
date: 2023-12-11
|
||||||
author: Juan Manzanero
|
author: Juan Manzanero
|
||||||
@ -56,7 +56,7 @@ next features:
|
|||||||
- Customers can just create a profile with their name or alias, and set
|
- Customers can just create a profile with their name or alias, and set
|
||||||
locations, for example: house
|
locations, for example: house
|
||||||
- Once a peddler is near, the backend will send a notification to the customer
|
- Once a peddler is near, the backend will send a notification to the customer
|
||||||
- If the customer taps and confirms the notification
|
- The customer taps and confirms the notification
|
||||||
|
|
||||||
Of course, customers would change notifications settings and more, but that's
|
Of course, customers would change notifications settings and more, but that's
|
||||||
the core idea.
|
the core idea.
|
1201
src/content/portfolio/es/construye-una-app-fullstack.mdx
Normal file
1201
src/content/portfolio/es/construye-una-app-fullstack.mdx
Normal file
File diff suppressed because it is too large
Load Diff
49
src/content/portfolio/es/human-to-js.mdx
Normal file
49
src/content/portfolio/es/human-to-js.mdx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Human to JS
|
||||||
|
description: ¡Traduce lenguaje humano a código JavaScript!
|
||||||
|
tags: [ChatGPT, Next.js, JavaScript, Vercel]
|
||||||
|
image: /portfolio/human-to-js/banner.png
|
||||||
|
imageCaption: Human to JS Banner
|
||||||
|
date: 2023-4-14
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Human to JS diagram_
|
||||||
|
|
||||||
|
_Este proyecto ha sido archivado._
|
||||||
|
|
||||||
|
## Retrospectiva
|
||||||
|
|
||||||
|
Siempre estoy buscando crecer mi carrera aprendiendo nuevas tecnologías como buen Ingeniero en Software; sin embargo, eso puede ser peligroso ya que la Ingeniería en Software no es sobre usar el último tech stack, sino de hacer que las cosas ocurran.
|
||||||
|
|
||||||
|
## Fuente de inspiración
|
||||||
|
|
||||||
|
Estaba revisando Twitter hasta que encontré un tweet de una persona que había creado un **side project** en un fin de semana. Ese proyecto es [SQL Translator](https://www.sqltranslate.app/), una simple UI para escribir un texto describiendo una consulta, entonces obtienes la consulta en código SQL, ¡simple!
|
||||||
|
|
||||||
|
[@woiskatring](https://twitter.com/whoiskatrin?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1634973237829599233%7Ctwgr%5Eb49b9d28e6ea7383ef16ea3c8c6040656ff0c944%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Fquery%3Dhttps3A2F2Ftwitter.com2Fwhoiskatrin2Fstatus2F1634973237829599233widget%3DTweet) usó la API de ChatGPT para enviar un prompt escrito por el user, devolviendo una respuesta en SQL dentro de un component para copiar al portapapeles. Eso fue suficiente para obtener la atención merecida por la comunidad. [Tweet link](https://twitter.com/whoiskatrin/status/1634973237829599233))
|
||||||
|
|
||||||
|
## Mi idea
|
||||||
|
|
||||||
|
> _¿Por qué no una web app para escribir un prompt y genera código JavaScript?_
|
||||||
|
|
||||||
|
Así que comencé a construir mi idea usando este tech stack:
|
||||||
|
|
||||||
|
- **Next.js**: Framework web para construir la UI y como Next.js provee un directorio API para comunicarte con la API de ChatGPT
|
||||||
|
- **MUI**: Para usar los UI components como sistema de diseño
|
||||||
|
- **Fromik & Yup**: Para manejar el estado del formulario del prompt y crear esquemas de validación
|
||||||
|
- **react-syntax-highlighter**: Para mostrar la respuesta de ChatGPT
|
||||||
|
|
||||||
|
Usando todas estas tecnologías construí una UI simple usando el MUI Card component, entonces creé el form donde manejé todos los inputs con Formik, los inputs de tipo text y select son los por defecto de MUI, y para crear el esquema de validación usé Yup para marcar como required aquellos inputs para no enviarlos vacíos.
|
||||||
|
|
||||||
|
Con la UI finalizada, comencé a crear el endpoint en el directorio de la API para consumir la API de ChatGPT, tan solo usando un fetch como en el proyecto de [@whoiskatrin](https://twitter.com/whoiskatrin?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1634973237829599233%7Ctwgr%5Eb49b9d28e6ea7383ef16ea3c8c6040656ff0c944%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fpublish.twitter.com%2F%3Fquery%3Dhttps3A2F2Ftwitter.com2Fwhoiskatrin2Fstatus2F1634973237829599233widget%3DTweet). indicando qué modelo de OpenAI utilizar, en este caso _text-davinci-003_, puedes aprender más de los esos modelos [aquí](https://platform.openai.com/docs/api-reference/models/list). Obviamente en la petición envié el prompt del user dentro de un string indicando a ChatGPT que solo me de el código, sin comentarios o resultados adicionales.
|
||||||
|
|
||||||
|
## Valor añadido
|
||||||
|
|
||||||
|
Sí, hay opciones como GitHub Copilot que resuelven ese problema, y eso es por qué agregué la opción de escoger qué tipo de sintaxis usar, arrow function o simple function.
|
||||||
|
|
||||||
|
Añadiré más features, como una opción para TypeScript, y usar una TS Interface para que se use como referencia, ¡pero ahora estoy trabajando en más proyectos!
|
||||||
|
|
||||||
|
## Inspirando personas
|
||||||
|
|
||||||
|
Lo genial de los side projects is que inspira personas como nosotros, podemos usar nuestras habilidades técnicas que llevan el pan a la mesa para transformar ideas en productos, y productos en comunidades, tal como [@Serudda](https://twitter.com/serudda) habla en este [video](https://www.youtube.com/watch?v=LXgPNdw8avI&t).
|
139
src/content/portfolio/es/next-intl-blog-template.mdx
Normal file
139
src/content/portfolio/es/next-intl-blog-template.mdx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
title: Next Intl Blog Template
|
||||||
|
description: ¡Comienza tu blog en múltiples idiomas!
|
||||||
|
tags: [Next.js, next-intl, tailwindcss]
|
||||||
|
image: /portfolio/next-intl-blog-template/banner.png
|
||||||
|
imageCaption: Next Intl Blog Template banner
|
||||||
|
date: 2023-12-18
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
_Next Intl Blog Template banner_
|
||||||
|
|
||||||
|
[GitHub](https://github.com/juancmandev/next-intl-blog-template)
|
||||||
|
|
||||||
|
[Website](https://next-intl-blog-template.vercel.app/en)
|
||||||
|
|
||||||
|
## Introducción
|
||||||
|
|
||||||
|
Recientemente actualicé este website, y como sabrás, es un website con **contenido en Inglés y Español**.
|
||||||
|
|
||||||
|
No estoy usando un plugin de traducción, en su lugar escribo cada palabra en ambos idiomas.
|
||||||
|
|
||||||
|
Gracias a Next.js y [next-intl](https://next-intl-docs.vercel.app/) puedo lograr esto, renderizando rutas para cada idioma en el website, accediendo a un diccionario que contiene el contenido traducido por mí.
|
||||||
|
|
||||||
|
Para los archivos .mdx, creé un directorio para cada idioma, y dentro de esos directorios contiene el contenido en ambos idiomas también.
|
||||||
|
|
||||||
|
## Cómo usar
|
||||||
|
|
||||||
|
Este template es una extensión de [next-intl](https://next-intl-docs.vercel.app/), revisa la [guía de inicio](https://next-intl-docs.vercel.app/docs/getting-started) para aprender lo básico, el propósito del template es crear un layout simple para futuras personalizaciones.
|
||||||
|
|
||||||
|
### Agregar o quitar locales
|
||||||
|
|
||||||
|
Puedes agregar o remover locales en el archivo `src/lang/locales.ts`.
|
||||||
|
|
||||||
|
```ts title="src/lang/locales.ts"
|
||||||
|
export type locales = "en" | "es";
|
||||||
|
|
||||||
|
export const localesList: locales[] = ["en", "es"];
|
||||||
|
```
|
||||||
|
|
||||||
|
Solo agrega o remueve un locale de la constante `locales`, y agrega o remueve el locale de la lista.
|
||||||
|
|
||||||
|
El primer item en `localesList` debe ser el locale por default.
|
||||||
|
|
||||||
|
La lista es usada para la generación de rutas estáticas en
|
||||||
|
`src/app/[locale]/layout.tsx`.
|
||||||
|
|
||||||
|
```ts title="src/app/[locale]/layout.tsx"
|
||||||
|
import { localesList } from "@/lang/locales";
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return localesList.map((locale) => ({ locale }));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Recuerda actualizar el matcher en `src/middleware.ts`.
|
||||||
|
|
||||||
|
```ts title="src/middleware.ts"
|
||||||
|
//...
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ["/", "/(en|es)/:path*"],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Y por supuesto, actualiza los archivos `src/lang/[locale].json`.
|
||||||
|
|
||||||
|
### Crear contenido
|
||||||
|
|
||||||
|
Usa `src/content/[locale]` para crear contenido, en el directorio `/[locale]/` crea un directorio para cada propósito, por ejemplo: `/[locale]/blog`.
|
||||||
|
|
||||||
|
Dentro crea el archivo .mdx con un nombre único, el nombre será usado como slug para crear la página estática para ese post.
|
||||||
|
|
||||||
|
Para crear una sección de blog, usarás la función _getAllContent_ en tu ruta, por ejemplo: `src/app/[locale]/blog/[slug]/page.tsx`.
|
||||||
|
|
||||||
|
```tsx title="src/app/[locale]/blog/[slug]/page.tsx"
|
||||||
|
import { Mdx } from "@/components";
|
||||||
|
import { TParamsLocale, TPage, TSlugLang } from "@/types";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { getAllContent, getContent } from "@/utils/getContent";
|
||||||
|
|
||||||
|
export async function generateStaticParams(
|
||||||
|
props: TParamsLocale,
|
||||||
|
): Promise<TSlugLang[]> {
|
||||||
|
const blogs = await getAllContent(props.params.locale, "blog");
|
||||||
|
|
||||||
|
if (!blogs) return [];
|
||||||
|
|
||||||
|
return blogs.map((blog) => ({
|
||||||
|
slug: blog.slug,
|
||||||
|
locale: props.params.locale,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto creará una página estática para cada post de blog.
|
||||||
|
|
||||||
|
Puedes obtener la metadata del archivo `.mdx` también.
|
||||||
|
|
||||||
|
```tsx title="src/app/[locale]/blog/[slug]/page.tsx"
|
||||||
|
//...
|
||||||
|
|
||||||
|
export async function generateMetadata(props: TPage): Promise<Metadata> {
|
||||||
|
const blog = await getContent(props.params.locale, "blog", props.params.slug);
|
||||||
|
|
||||||
|
if (!blog) return {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: blog.title,
|
||||||
|
//...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
Entonces, renderiza el contenido usando el componente _Mdx_.
|
||||||
|
|
||||||
|
```tsx title="src/app/[locale]/blog/[slug]/page.tsx"
|
||||||
|
//...
|
||||||
|
|
||||||
|
export default async function Page(props: TPage) {
|
||||||
|
const post = await getContent(props.params.locale, "blog", props.params.slug);
|
||||||
|
|
||||||
|
if (!post) return null;
|
||||||
|
|
||||||
|
return <Mdx code={post.body.code} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Puedes hacer un fork de este template aquí](https://github.com/juancmandev/next-intl-blog-template)
|
||||||
|
|
||||||
|
## Contacto
|
||||||
|
|
||||||
|
Si te interesa **trabajar juntos** en un website con internacionalización con Next.js, envíame un correo a [contact@juancman.dev](mailto:contact@juancman.dev)
|
54
src/content/portfolio/es/peddler-app.mdx
Normal file
54
src/content/portfolio/es/peddler-app.mdx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: Peddler App
|
||||||
|
description: Escuchas al heladero en su camioneta, intentas alcanzarlo, pero ya está lejos.
|
||||||
|
tags: [Tech, SideProject]
|
||||||
|
image: /blog/peddler-app/banner.png
|
||||||
|
imageCaption: Logo provisional de Peddler App
|
||||||
|
date: 2023-12-11
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Logo provisional de Peddler App_
|
||||||
|
|
||||||
|
## La idea
|
||||||
|
|
||||||
|
Escuchas al heladero en su camioneta, intentas alcanzarlo, pero ya está lejos.
|
||||||
|
|
||||||
|
¿Por qué no recibir una notificación en tu celular cuando el heladero está cerca de ti?, así puedes tocar la
|
||||||
|
notificación y solicitar al heladero que pase en tu ubicación.
|
||||||
|
|
||||||
|
Ese es el propósito de esta app.
|
||||||
|
|
||||||
|
## Abrumador para mi
|
||||||
|
|
||||||
|
Nunca he desarrollado una app grande por mi mismo, pero quiero intentarlo y ver qué ocurre.
|
||||||
|
|
||||||
|
Quiero seguir el camino Indie Hacker, compartiendo en público el progreso, y recibiendo feedback
|
||||||
|
de la comunidad.
|
||||||
|
|
||||||
|
Este post es el primer paso antes de diseñarlo en Figma o incluso crear la landing page, así que
|
||||||
|
quiero escuchar si te interesa, por qué no, o que sería genial para la app.
|
||||||
|
|
||||||
|
## Empezando pequeño
|
||||||
|
|
||||||
|
Realmente quiero empezar pequeño, lanzando un Minimum Viable Product (MVP), con las siguientes funcionalidades:
|
||||||
|
|
||||||
|
- Registro de usuarios y login
|
||||||
|
- Dos tipos de usuarios, "peddlers" (vendedores ambulantes) y clientes
|
||||||
|
- Peddlers
|
||||||
|
- Si el usuario es peddler (quiere vender), redireccionar al formulario de peddlers
|
||||||
|
- Peddlers pueden crear un perfil con el nombre de su empresa y productos que ofrece, por ejemplo helado, dulces, etc
|
||||||
|
- Una vez que el registro es finalizado, peddlers pueden iniciar rutas
|
||||||
|
- La app obtiene la ubicación del peddler, mostrándolo en el mapa
|
||||||
|
- El backend detectará si el peddler entra en el radio de un cliente, y enviará una notificación push al cliente
|
||||||
|
- Una vez que el peddler tiene una solicitud, la app mostrará la ubicación del cliente solicitante en el mapa
|
||||||
|
- El peddler puede ir a la ubicación, y completar la transacción
|
||||||
|
- Clientes
|
||||||
|
- Clientes pueden crear una perfil con su nombre o alias, y registrar ubicaciones, por ejemplo: casa
|
||||||
|
- Una vez que un peddler está cerca, el backend enviará una notificación push al cliente
|
||||||
|
- El cliente toca y confirma la notificación
|
||||||
|
|
||||||
|
Por supuesto, clientes pueden cambiar la configuración de las notificaciones, pero esa es la idea core.
|
||||||
|
|
||||||
|
Para el MVP no quiero implementar pagos dentro de la app, pero desde luego, podría ser un feature futuro.
|
40
src/content/portfolio/es/workarise.mdx
Normal file
40
src/content/portfolio/es/workarise.mdx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Workarise
|
||||||
|
description: Workarise Web App, administra tareas con tu equipo.
|
||||||
|
tags: [React.js, Vite.js, MUI, Firebase, GCP, Node.js]
|
||||||
|
image: /portfolio/workarise/banner.png
|
||||||
|
imageCaption: Workarise Banner
|
||||||
|
date: 2023-4-13
|
||||||
|
author: Juan Manzanero
|
||||||
|
rss: true
|
||||||
|
---
|
||||||
|
|
||||||
|
 _Workarise Banner_
|
||||||
|
|
||||||
|
[Website](https://workarise.com)
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
[Workarise](https://workarise.com) es un Software como Servicio para administrar equipos mediante task cards asignadas a personas, establece una fecha de inicio y final, agregar archivos, etc.
|
||||||
|
|
||||||
|
Puedes usar el Calendario para ver las fechas de entrega de las tareas y agendar eventos de Google Meet autorizando el uso de tu Google Calendar. Puedes editar y borrar eventos los cuales se sincronizan con tu Google Calendar y el de los invitados. El Gantt te provee con una línea del tiempo para ver la duración de las tareas.
|
||||||
|
|
||||||
|
La web app está desarrollada con [React.js](https://react.dev/), usando [Vite.js](https://vitejs.dev/) para correr el entorno de desarrollo. Para funcionalidades como modals y popovers usamos [MUI](https://mui.com/). Para manejar el estado de los componentes usamos useContext.
|
||||||
|
|
||||||
|
Para crear eventos Google Meet y sincronizar el calendario hemos desarrollado una pequeña API con Node.js para usar la API de Google OAuth 2, ya que necesitamos pedir permiso para acceder al Google Calendar del user.
|
||||||
|
|
||||||
|
Actualmente, Workarise está en su primera versión, usando [Firebase](https://firebase.google.com/) para autenticar a los users, guardar datos y archivos. Firebase utiliza [Firestore](https://firebase.google.com/docs/firestore), una Base de Datos NoSQL, sin embargo estamos desarrollando una API con [Django](https://www.djangoproject.com/) corriendo en [Cloud Run](https://cloud.google.com/run)) conectado a [Cloud SQL](https://cloud.google.com/sql) a una instancia MySQL, ya que usaremos una Base de Datos SQL en el futuro. Actualmente la web app y la landing están desplegadas en Firebase Hosting, pero moveremos la landing a Vercel, y será actualizada a Next.js in el futuro para optimizar el SEO y publicar blog posts.
|
||||||
|
|
||||||
|
## Mi impacto en Workarise
|
||||||
|
|
||||||
|
Actualmente, estamos desarrollando un MVP, y todos estamos a tiempo parcial en este proyecto. Me uní en Diciembre, pero antes no había un producto que los users pudieran usar, y como era el único Ingeniero Frontend en ese momento tomé toda la responsabilidad de entregar algo que pudiera considerarse un MVP.
|
||||||
|
|
||||||
|
Me tomó alrededor de 3 meses para conseguir eso, actualicé algunas de las dependencias del proyecto para mejorar el flujo de desarrollo, y sugerí usar Firebase como Backend y Hosting.
|
||||||
|
|
||||||
|
Gracias a todo esto obtuvimos nuestros primeros users y feedback, así que ahora estamos trabajando con ese feedback para mantener mejorando nuestra app, ¡a nuestros users les gusta el diseño y la simplicidad!
|
||||||
|
|
||||||
|
Ahora mismo hay 3 engineers en el equipo, 2 en el front (incluyéndome) y 1 en el back, pero estoy ayudando a nuestra Backend Engineer para desplegar en GCP a producción la API y Base de Datos, y estoy guiando al nuevo Frontend Engineer para entregar nuevos features, él ha desarrollado el responsive design y algunos features que complementan las task cards.
|
||||||
|
|
||||||
|
Estoy feliz de poner a prueba mis habilidades en este proyecto, no es fácil tomar más responsabilidad con menos de 2 años de experiencia laboral, y me ha ayudado a crecer mucho en estos meses.
|
||||||
|
|
||||||
|
Incluso si el mercado no considera mis años como senior, creo que eso no importa tanto, lo único que importa es que puedas entender por qué usas código, para crear soluciones y alcanzar a las personas a través de sus computadoras.
|
@ -1,29 +1,184 @@
|
|||||||
---
|
---
|
||||||
title: Nadie Entiende la Privacidad
|
title: Nadie Entiende la Privacidad
|
||||||
description:
|
description: Hablar de privacidad es complicado, ya que muy pocos la entienden.
|
||||||
Hablar de privacidad es complicado, ya que no todo el mundo la entiende de
|
|
||||||
verdad.
|
|
||||||
tags:
|
tags:
|
||||||
- Tech
|
- Tech
|
||||||
- Freedom
|
- Freedom
|
||||||
- Libre
|
- Libre
|
||||||
image: https://img.youtube.com/vi/Wlw6rscU4gI/maxresdefault.jpg
|
image: /blog/how-computers-works/banner.jpg
|
||||||
imageCaption: Video thumbnail.
|
imageCaption: Video thumbnail.
|
||||||
date: 6/3/2024
|
date: 7/25/2024
|
||||||
author: Juan Manzanero
|
author: Juan Manzanero
|
||||||
rss: true
|
rss: true
|
||||||
---
|
---
|
||||||
|
|
||||||
<iframe
|
 _An open laptop.
|
||||||
width='100%'
|
Photo by
|
||||||
height='320'
|
[Philipp Katzenberger](https://unsplash.com/@fantasyflip?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText')
|
||||||
className='rounded-md'
|
on
|
||||||
src='https://www.youtube-nocookie.com/embed/_x6SCSz7g5I'
|
[Unsplash](https://unsplash.com/photos/iIJrUoeRoCQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)_
|
||||||
title='YouTube video player'
|
|
||||||
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
|
|
||||||
allowFullScreen
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
Cuando respecto a la privacidad, por lo general la respuesta de la gente es "como si tuviese algo que ocultar" o "es imposible ocultar todo lo que hacemos", y es justo a esas frases cuando digo que nadie entiende la privacidad.
|
## Lo que los normies dicen al escuchar "privacidad"
|
||||||
|
|
||||||
La privacidad es una necesidad humana, un derecho, está en nuestra naturaleza. Prueba de ello, cuando vas a un baño público, generalmente cierras la puerta, o intentas usar un espacio donde no haya gente cerca. Y es que todos sabemos qué se hace cuando se va al baño, pero todos queremos tener un momento de privacidad para hacer nuestras necesidades, ya que es algo que no queremos compartir con extraños.
|
"¿Por qué te interesa la privacidad?, ¿acaso haces algo ilegal?"
|
||||||
|
|
||||||
|
"A mi no me interesa la privacidad, ni que tuviese algo que esconder."
|
||||||
|
|
||||||
|
"De todas formas somos espiados, no vale la pena preocuparse por la privacidad."
|
||||||
|
|
||||||
|
Estas son solo algunas de las respuestas más comunes cuando se menciona la
|
||||||
|
palabra "privacidad", pero esto solo aplica en el ecosistema digital.
|
||||||
|
|
||||||
|
## No es que tengas algo que ocultar, la privacidad es un derecho humano
|
||||||
|
|
||||||
|
Curioso, porque la mayoria de las personas cuando vamos al baño, cerramos la
|
||||||
|
puerta; son muy contadas las personas a las que permitiríamos entrar.
|
||||||
|
|
||||||
|
¿Por qué?, digo, todos sabemos lo que se hace en un baño, pero aún así, es un
|
||||||
|
espacio personal donde queremos estar a solas.
|
||||||
|
|
||||||
|
Y es que de eso se trata la privacidad, es una necesidad humana, un derecho.
|
||||||
|
|
||||||
|
La gente confunde privacidad con secretismo, creen que por querer ocultar algo,
|
||||||
|
es porque sabemos que es malo.
|
||||||
|
|
||||||
|
Pero ir al baño a satisfacer nuestras necesidas fisiológicas no es algo ilegal.
|
||||||
|
|
||||||
|
Entonces, ¿por qué la gente en general es tan cínica con respecto a la
|
||||||
|
privacidad?
|
||||||
|
|
||||||
|
## La conveniencia de los servicios de las Big Tech a cambio de tus datos
|
||||||
|
|
||||||
|
Bueno, no es sorpresa que la mayoría de estas personas son normies, gente que
|
||||||
|
usa Windows/macOS en sus PCs, quienes usan todos los servicios "gratuitos" de
|
||||||
|
Google, Facebook, Microsoft y Apple.
|
||||||
|
|
||||||
|
Estas empresas, las conocidas como "Big Tech" no son organizaciones sin ánimos
|
||||||
|
de lucro que crean servicios gratuitos, para nada.\_createMdxContent
|
||||||
|
|
||||||
|
Al contrario, son de las empresas más poderosas que han existido en la historia
|
||||||
|
de la humanidad, y su valor en la bolsa no deja de crecer.
|
||||||
|
|
||||||
|
Y es simple, aunque puedas pagar por algunas funcionalidades premium en sus
|
||||||
|
servicios, la realidad es que su verdadero producto eres **TÚ**, para ser más
|
||||||
|
específico, tus datos.
|
||||||
|
|
||||||
|
Google Chrome y Microsoft Edge no son gratuitos, recolectan tus datos para
|
||||||
|
venderte anuncios personalizados.
|
||||||
|
|
||||||
|
Tus fotos que guardas en Google Photos son analizadas por Google, pero dirás que
|
||||||
|
no te tomas fotos sin ropa...
|
||||||
|
|
||||||
|
## No hizo algo ilegal, pero fue castigado
|
||||||
|
|
||||||
|
No necesariamente debes evitar subir ese tipo de fotos (sin prejuicios), prueba
|
||||||
|
de ello está este caso de un padre de un bebé en San Francisco, California.
|
||||||
|
|
||||||
|
En resumidas cuentas, un padre tomó fotos de los genitales de su hijo debido a
|
||||||
|
un problema, se las envió a su doctor de confianza para que realizara un
|
||||||
|
diagnóstico (esto fue durante la pandemia, en 2021).
|
||||||
|
|
||||||
|
El problema fue que el padre tenía su celular con Android con el backup de fotos
|
||||||
|
con Google Photos, y dicha foto fue subida a los servers de Google.
|
||||||
|
|
||||||
|
Google desde luego tiene bots que analizan las fotos que son subidas para evitar
|
||||||
|
almacenar cosas ilegales, como pornografía infantil, y es debido a esto que el
|
||||||
|
padre fue flaggeado por el algoritmo de Google como un criminal.
|
||||||
|
|
||||||
|
Su cuenta de Google fue deshabilitada, perdió acceso a su celular y a todos sus
|
||||||
|
datos que tenía almacenados en los servicios de Google, y desde luego, Google
|
||||||
|
notificó a la policía.
|
||||||
|
|
||||||
|
Después del juicio, el jurado declaró que no se había cometido crimen alguno,
|
||||||
|
pero Google se negó a restaurar el acceso a la cuenta del padre.
|
||||||
|
|
||||||
|
Esta persona no tenía algo que esconder, y aún así fue castigado por una Big
|
||||||
|
Tech, no solo prohibiendo acceso a su cuenta, sino incluso notificando a la
|
||||||
|
policía.
|
||||||
|
|
||||||
|
Y este es el problema cuando delegas tus fotos a una empresa que analiza cada
|
||||||
|
imagen que subes, estás a la merced de los "Términos y Condiciones", o en este
|
||||||
|
caso, a la interpretación de un bot de los Términos y Condiciones.
|
||||||
|
|
||||||
|
Desde luego, nadie quiere que usen los servers de Google para guardar contenido
|
||||||
|
ilegal, pero estas medidas terminan afectando a personas que no cometieron un
|
||||||
|
crimen.
|
||||||
|
|
||||||
|
## Preocuparse por la privacidad es como preocuparse por tu salud, poco a poco antes que sea demasiado tarde
|
||||||
|
|
||||||
|
Lo sé, comenzar a preocuparse por la privacidad es intimidante, ya que es un
|
||||||
|
tema en el que hay que indagar bastante, ya que la mayoría de las personas son
|
||||||
|
muy ignorantes al respecto, pero no es necesario formatear todos tus drivers y
|
||||||
|
comenzar a usar alguna distro de Linux enfocada en privacidad desde el día cero.
|
||||||
|
Puedes comenzar dando pequeños pasos.
|
||||||
|
|
||||||
|
Este tema no es uno de blanco y negro, de ignorancia total o absoluta paranoia,
|
||||||
|
es más bien un espectro, en donde puedes situarte poco a poco más del lado de la
|
||||||
|
privacidad, pero sin llegar a ser extremista.
|
||||||
|
|
||||||
|
## Primeros pasos y alternativas
|
||||||
|
|
||||||
|
Por ejemplo, comienza cambiando tu navegador de Internet, en lugar de Chrome o
|
||||||
|
Edge, podrías usar Brave, o mejor aún, Firefox con un User.js como BetterFox,
|
||||||
|
otra alternativa es LibreWolf, un fork de Firefox con mejoras en seguridad y
|
||||||
|
privacidad.
|
||||||
|
|
||||||
|
Igual lo ideal es que no uses el gestor de contraseñas que viene por defecto en
|
||||||
|
tu navegador, lo ideal es que uses uno independiente como Bitwarden o Proton
|
||||||
|
Pass, estos cuentan con add-ons para navegadores basados en Chrome y Firefox.
|
||||||
|
|
||||||
|
De igual manera un gran paso que puedes tomar es cambiando tu email en lugar de
|
||||||
|
utilizar Outlook o Gmail, una buena alternativa es Proton Mail.
|
||||||
|
|
||||||
|
A su vez, intenta usar menos redes sociales, o como mínimo no compartas lo que
|
||||||
|
estás haciendo en ese momento, ya que puedes revelar información que no te
|
||||||
|
gustaría que sea usada en tu contra.
|
||||||
|
|
||||||
|
## Sigue así
|
||||||
|
|
||||||
|
Una vez que hayas tomados estos primeros pasos, lo ideal es que liberes por
|
||||||
|
completo tus dispositivos.
|
||||||
|
|
||||||
|
En tu computadora para empezar, instalando alguna distro de Linux, una muy buena
|
||||||
|
para gente novata en Linux es Linux Mint, basada en una de las distros más
|
||||||
|
populares (Ubuntu) pero sin las malas decisiones que ha implementado Canonical
|
||||||
|
(la empresa detrás de Ubuntu).
|
||||||
|
|
||||||
|
De igual manera tu celular es un dispositivo muy delicado, y desafortunadamente
|
||||||
|
difícil de liberar.
|
||||||
|
|
||||||
|
Para empezar, si usas un iPhone, lo mejor que podrías hacer es jailbreakearlo,
|
||||||
|
pero no es muy viable usar un iPhone si lo que te preocupa es la privacidad.
|
||||||
|
|
||||||
|
Y sí, Apple profesa mucho que los iPhones son seguros, pero eso es mentira
|
||||||
|
(hablaré más de esto en el futuro).
|
||||||
|
|
||||||
|
El dispositivo ideal sería un Android, en específico un Google Pixel reciente,
|
||||||
|
ya que estos se pueden instalar un Custom ROM de Android, una buena opción es
|
||||||
|
GrapheneOS.
|
||||||
|
|
||||||
|
Repito, no hace falta que tomes todos estos pasos de una, puedes ir poco a poco.
|
||||||
|
|
||||||
|
## No caigas en la trampa del cinismo o el pesimismo
|
||||||
|
|
||||||
|
Pero dirás: "las empresas ya tienen mis datos, de nada me sirve preocuparme
|
||||||
|
ahora".
|
||||||
|
|
||||||
|
Eso es una estupidez, es como que alguien venga a golpearte y tu no te defiendas
|
||||||
|
ya que ya te golpearon; no, lo primero que haces es defenderte.
|
||||||
|
|
||||||
|
De eso se trata la privacidad hoy en día, es un tema de defensa de los derechos,
|
||||||
|
no querer ocultar actividades ilegales.
|
||||||
|
|
||||||
|
Curiosamente, los políticos se preocupan más por su privacidad, y no
|
||||||
|
necesariamente para ocultar sus obras de caridad, pero el gobierno es quien más
|
||||||
|
va a querer restar bloqueos para poder espiar a sus ciudadanos, tal es el caso
|
||||||
|
de Estados Unidos, o en casos más extremos; China y Corea del Norte.
|
||||||
|
|
||||||
|
En fin, espero que con este video pienses dos veces cuando escuches a un normie
|
||||||
|
que no tiene nada que esconder, con lo cual puedes responder "si bien no tengo
|
||||||
|
algo que esconder, tampoco tengo algo en particular que quiera compartir
|
||||||
|
contigo".
|
||||||
|
|
||||||
|
No permitas que 1984 de George Orwell pase de ser ciencia ficción a una
|
||||||
|
predicción del futuro.
|
||||||
|
44
src/i18n/ui.ts
Normal file
44
src/i18n/ui.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export const languages = {
|
||||||
|
en: "English",
|
||||||
|
es: "Español",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultLang = "en";
|
||||||
|
export const showDefaultLang = false;
|
||||||
|
|
||||||
|
export const ui = {
|
||||||
|
en: {
|
||||||
|
navigation: "Navigation",
|
||||||
|
"blog.label": "Blog",
|
||||||
|
"blog.to": "/blog",
|
||||||
|
"portfolio.label": "Portfolio",
|
||||||
|
"portfolio.to": "/portfolio",
|
||||||
|
"videos.label": "Videos",
|
||||||
|
"videos.to": "/es/videos",
|
||||||
|
"microblog.label": "Microblog",
|
||||||
|
"microblog.to": "/microblog",
|
||||||
|
"resources.label": "Resources",
|
||||||
|
"resources.to": "/resources",
|
||||||
|
"about.label": "About",
|
||||||
|
"about.to": "/about",
|
||||||
|
"contact.label": "Contact",
|
||||||
|
"contact.to": "/contact",
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
navigation: "Navegación",
|
||||||
|
"blog.label": "Blog",
|
||||||
|
"blog.to": "/es/blog",
|
||||||
|
"portfolio.label": "Portfolio",
|
||||||
|
"portfolio.to": "/es/portfolio",
|
||||||
|
"videos.label": "Videos",
|
||||||
|
"videos.to": "/es/videos",
|
||||||
|
"microblog.label": "Microblog",
|
||||||
|
"microblog.to": "/microblog",
|
||||||
|
"resources.label": "Recursos",
|
||||||
|
"resources.to": "/es/recursos",
|
||||||
|
"about.label": "Acerca de",
|
||||||
|
"about.to": "/es/acerca-de",
|
||||||
|
"contact.label": "Contacto",
|
||||||
|
"contact.to": "/es/contacto",
|
||||||
|
},
|
||||||
|
} as const;
|
19
src/i18n/utils.ts
Normal file
19
src/i18n/utils.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ui, defaultLang, showDefaultLang } from "@/i18n/ui";
|
||||||
|
|
||||||
|
export function getLangFromUrl(url: URL) {
|
||||||
|
const [, lang] = url.pathname.split("/");
|
||||||
|
if (lang in ui) return lang as keyof typeof ui;
|
||||||
|
return defaultLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslations(lang: keyof typeof ui) {
|
||||||
|
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
|
||||||
|
return ui[lang][key] || ui[defaultLang][key];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslatedPath(lang: keyof typeof ui) {
|
||||||
|
return function translatePath(path: string, l: string = lang) {
|
||||||
|
return !showDefaultLang && l === defaultLang ? path : `/${l}${path}`;
|
||||||
|
};
|
||||||
|
}
|
@ -1,45 +1,54 @@
|
|||||||
---
|
---
|
||||||
|
import Header from "@/components/header.astro";
|
||||||
|
import Navigation from "@/components/navigation";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import Navbar from "@/components/navbar.astro";
|
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
import { getLangFromUrl } from "../i18n/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
lang?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, lang } = Astro.props;
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
|
||||||
|
const { title, description } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<html lang={lang}>
|
||||||
<html lang={lang || "en"}>
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="description" content={description} />
|
||||||
<meta name="description" content={description} />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link
|
||||||
<link
|
rel="alternate icon"
|
||||||
rel="alternate icon"
|
href="/favicon.ico"
|
||||||
href="/favicon.ico"
|
type="image/png"
|
||||||
type="image/png"
|
sizes="16x16"
|
||||||
sizes="16x16"
|
/>
|
||||||
/>
|
<link
|
||||||
<link
|
rel="alternate"
|
||||||
rel="alternate"
|
title="juancmandev"
|
||||||
title="juancmandev"
|
type="application/rss+xml"
|
||||||
type="application/rss+xml"
|
href={new URL("feed.xml", Astro.site)}
|
||||||
href={new URL("rss.xml", Astro.site)}
|
/>
|
||||||
/>
|
<link
|
||||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
rel="alternate"
|
||||||
<meta name="generator" content={Astro.generator} />
|
title="juancmandev"
|
||||||
<title>{title}</title>
|
type="application/rss+xml"
|
||||||
</head>
|
href={new URL("feed.xml", `${Astro.site}/es/`)}
|
||||||
<body>
|
/>
|
||||||
<Navbar />
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
<main class="px-4 min-h-screen max-w-[65ch] py-28 mx-auto">
|
<meta name="generator" content={Astro.generator} />
|
||||||
<slot />
|
<title>{title}</title>
|
||||||
</main>
|
</head>
|
||||||
<Footer />
|
<body>
|
||||||
</body>
|
<Header lang={lang} />
|
||||||
|
<main class="px-4 sm:px-0 max-w-[65ch] pt-28 pb-5 mx-auto">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Navigation lang={lang} />
|
||||||
|
<Footer lang={lang} />
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import LinkButton from "@/components/link-button";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="juancmandev" description="Error 404. Not found.">
|
<Layout title="Not found" description="Error 404: Not found.">
|
||||||
<div class="prose prose-invert">
|
<div class="prose prose-invert">
|
||||||
<h1 class="">Error 404: Not found</h1>
|
<h1 class="">Error 404: Not found</h1>
|
||||||
<p>Do not worry, you can <strong>go back to home</strong>.</p>
|
</div>
|
||||||
<LinkButton variant="default" href="/" className="no-underline"
|
|
||||||
>Home</LinkButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import components from "@/components/mdx/wrapper";
|
import components from "@/components/mdx/wrapper";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: CollectionEntry<"pages">;
|
page: CollectionEntry<"pages">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPages = await getCollection("pages");
|
const allPages = await getCollection("pages");
|
||||||
|
|
||||||
return allPages.map((page: CollectionEntry<"pages">) => ({
|
return allPages.map((page: CollectionEntry<"pages">) => ({
|
||||||
params: { slug: page.slug },
|
params: { slug: page.slug },
|
||||||
props: { page },
|
props: { page },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { page } = Astro.props;
|
const { page } = Astro.props;
|
||||||
const { Content } = await page.render();
|
const { Content } = await page.render();
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={page.data.title} description={page.data.description}>
|
<Layout {...page.data}>
|
||||||
<article class="prose prose-invert">
|
<article class="prose prose-invert">
|
||||||
<Content components={{ ...components }} />
|
<Content components={{ ...components }} />
|
||||||
</article>
|
</article>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import components from "@/components/mdx/wrapper";
|
import components from "@/components/mdx/wrapper";
|
||||||
import formatDate from "@/utils/format-date";
|
import formatDate from "@/utils/format-date";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: CollectionEntry<"blog">;
|
post: CollectionEntry<"blog">;
|
||||||
@ -14,15 +15,27 @@ export async function getStaticPaths() {
|
|||||||
"blog",
|
"blog",
|
||||||
({ data }) => data.draft !== true,
|
({ data }) => data.draft !== true,
|
||||||
);
|
);
|
||||||
|
const filterEnPosts = allBlogPosts.map((post) => {
|
||||||
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
return allBlogPosts.map((post) => ({
|
if (lang === "en")
|
||||||
params: { slug: post.slug },
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterEnPosts.map((post) => ({
|
||||||
|
params: { slug: post?.slug },
|
||||||
props: { post },
|
props: { post },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render();
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={post.data.title} description={post.data.description}>
|
<Layout title={post.data.title} description={post.data.description}>
|
||||||
@ -32,7 +45,7 @@ const { Content } = await post.render();
|
|||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
<strong>Posted: </strong>
|
<strong>Posted: </strong>
|
||||||
{post.data.date && formatDate(new Date(post.data.date))}
|
{post.data.date && formatDate(new Date(post.data.date), lang)}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,30 +1,52 @@
|
|||||||
---
|
---
|
||||||
import PostItem from "@/components/post-item";
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import { sortContentByDate } from "@/utils/sorts";
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "Blog",
|
||||||
|
description: "Long format about thoughts and other topics.",
|
||||||
|
};
|
||||||
|
|
||||||
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||||
sortContentByDate(allPosts);
|
const filterEnPosts = allPosts.map((post) => {
|
||||||
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "en")
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(filterEnPosts);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Blog" description="Check my projects.">
|
<Layout {...pageData}>
|
||||||
<section class="prose prose-invert">
|
<section class="prose prose-invert">
|
||||||
<h1>Blog</h1>
|
<h1>{pageData.title}</h1>
|
||||||
<p>Long format about thoughts and other topics.</p>
|
<p>{pageData.description}</p>
|
||||||
</section>
|
</section>
|
||||||
<ul class="mt-4 flex flex-col gap-4">
|
<ul class="mt-4 flex flex-col gap-4">
|
||||||
{
|
{
|
||||||
allPosts.map((blogpost: any) => (
|
filterEnPosts.map(
|
||||||
<li>
|
(blogpost) =>
|
||||||
<PostItem
|
blogpost && (
|
||||||
type="blog"
|
<li>
|
||||||
slug={blogpost.slug}
|
<PostItem
|
||||||
date={blogpost.data.date!}
|
type="blog"
|
||||||
title={blogpost.data.title!}
|
lang={lang}
|
||||||
/>
|
slug={blogpost.slug}
|
||||||
</li>
|
date={blogpost.data.date!}
|
||||||
))
|
title={blogpost.data.title!}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
28
src/pages/es/[...slug].astro
Normal file
28
src/pages/es/[...slug].astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import components from "@/components/mdx/wrapper";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: CollectionEntry<"pages">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPages = await getCollection("pages");
|
||||||
|
|
||||||
|
return allPages.map((page: CollectionEntry<"pages">) => ({
|
||||||
|
params: { slug: page.slug },
|
||||||
|
props: { page },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
const { Content } = await page.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout {...page.data}>
|
||||||
|
<article class="prose prose-invert">
|
||||||
|
<Content components={{ ...components }} />
|
||||||
|
</article>
|
||||||
|
</Layout>
|
51
src/pages/es/blog/[...slug].astro
Normal file
51
src/pages/es/blog/[...slug].astro
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import components from "@/components/mdx/wrapper";
|
||||||
|
import formatDate from "@/utils/format-date";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
post: CollectionEntry<"blog">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allBlogPosts = await getCollection(
|
||||||
|
"blog",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
const filterEsPosts = allBlogPosts.map((post) => {
|
||||||
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterEsPosts.map((post) => ({
|
||||||
|
params: { slug: post?.slug },
|
||||||
|
props: { post },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const { Content } = await post.render();
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={post.data.title} description={post.data.description}>
|
||||||
|
<article class="prose prose-invert">
|
||||||
|
<h1>{post.data.title}</h1>
|
||||||
|
<Content components={{ ...components }} />
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>Publicado: </strong>
|
||||||
|
{post.data.date && formatDate(new Date(post.data.date), lang)}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Layout>
|
52
src/pages/es/blog/index.astro
Normal file
52
src/pages/es/blog/index.astro
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "Blog",
|
||||||
|
description: "Formato largo sobre pensamientos y otros temas.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||||
|
const filterEsPosts = allPosts.map((post) => {
|
||||||
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(filterEsPosts);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout {...pageData}>
|
||||||
|
<section class="prose prose-invert">
|
||||||
|
<h1>{pageData.title}</h1>
|
||||||
|
<p>{pageData.description}</p>
|
||||||
|
</section>
|
||||||
|
<ul class="mt-4 flex flex-col gap-4">
|
||||||
|
{
|
||||||
|
filterEsPosts.map(
|
||||||
|
(post) =>
|
||||||
|
post && (
|
||||||
|
<li>
|
||||||
|
<PostItem
|
||||||
|
type="blog"
|
||||||
|
lang={lang}
|
||||||
|
slug={post?.slug}
|
||||||
|
date={post?.data.date!}
|
||||||
|
title={post?.data.title!}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Layout>
|
140
src/pages/es/feed.xml.ts
Normal file
140
src/pages/es/feed.xml.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import rss from "@astrojs/rss";
|
||||||
|
import type { RSSFeedItem } from "@astrojs/rss";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import { parse as htmlParser } from "node-html-parser";
|
||||||
|
import { getImage } from "astro:assets";
|
||||||
|
import type { ImageMetadata } from "astro";
|
||||||
|
|
||||||
|
const markdownParser = new MarkdownIt();
|
||||||
|
|
||||||
|
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
|
||||||
|
"/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}"
|
||||||
|
);
|
||||||
|
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
|
||||||
|
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}"
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function GET(context: any) {
|
||||||
|
const items: RSSFeedItem[] = [];
|
||||||
|
|
||||||
|
const blog = await getCollection(
|
||||||
|
"blog",
|
||||||
|
({ data }) => data.draft !== true && data.rss === true
|
||||||
|
);
|
||||||
|
const filterBlog = blog.filter((post) => {
|
||||||
|
const [lang] = post.slug.split("/");
|
||||||
|
|
||||||
|
return lang === "es" && post;
|
||||||
|
});
|
||||||
|
|
||||||
|
const portfolio = await getCollection(
|
||||||
|
"portfolio",
|
||||||
|
({ data }) => data.draft !== true && data.rss === true
|
||||||
|
);
|
||||||
|
const filterPortfolio = portfolio.filter((project) => {
|
||||||
|
const [lang] = project.slug.split("/");
|
||||||
|
|
||||||
|
return lang === "es" && project;
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const post of filterBlog) {
|
||||||
|
const body = markdownParser.render(post.body);
|
||||||
|
const html = htmlParser.parse(body);
|
||||||
|
const images = html.querySelectorAll("img");
|
||||||
|
|
||||||
|
for await (const img of images) {
|
||||||
|
const src = img.getAttribute("src")!;
|
||||||
|
|
||||||
|
if (src.startsWith("@/")) {
|
||||||
|
const prefixRemoved = src.replace("@/", "");
|
||||||
|
const imagePathPrefix = `/src/${prefixRemoved}`;
|
||||||
|
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
|
||||||
|
(res: any) => res.default
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
const optimizedImg = await getImage({ src: imagePath });
|
||||||
|
img.setAttribute(
|
||||||
|
"src",
|
||||||
|
context.site + optimizedImg.src.replace("/", "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (src.startsWith("/images")) {
|
||||||
|
img.setAttribute("src", context.site + src.replace("/", ""));
|
||||||
|
} else {
|
||||||
|
throw Error("src unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: post.data.title,
|
||||||
|
pubDate: post.data.date,
|
||||||
|
description: post.data.description,
|
||||||
|
link: `/blog/${post.slug}/`,
|
||||||
|
content: sanitizeHtml(html.toString(), {
|
||||||
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const project of filterPortfolio) {
|
||||||
|
const body = markdownParser.render(project.body);
|
||||||
|
const html = htmlParser.parse(body);
|
||||||
|
const images = html.querySelectorAll("img");
|
||||||
|
|
||||||
|
for await (const img of images) {
|
||||||
|
const src = img.getAttribute("src")!;
|
||||||
|
|
||||||
|
if (src.startsWith("@/")) {
|
||||||
|
const prefixRemoved = src.replace("@/", "");
|
||||||
|
const imagePathPrefix = `/src/${prefixRemoved}`;
|
||||||
|
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
|
||||||
|
(res: any) => res.default
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
const optimizedImg = await getImage({ src: imagePath });
|
||||||
|
img.setAttribute(
|
||||||
|
"src",
|
||||||
|
context.site + optimizedImg.src.replace("/", "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (src.startsWith("/images")) {
|
||||||
|
// images starting with `/images/` is the public dir
|
||||||
|
img.setAttribute("src", context.site + src.replace("/", ""));
|
||||||
|
} else {
|
||||||
|
throw Error("src unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: project.data.title,
|
||||||
|
pubDate: project.data.date,
|
||||||
|
description: project.data.description,
|
||||||
|
link: `/portfolio/${project.slug}/`,
|
||||||
|
content: sanitizeHtml(html.toString(), {
|
||||||
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
xmlns: { atom: "http://www.w3.org/2005/Atom" },
|
||||||
|
title: "juancmandev",
|
||||||
|
description: "Bienvenido a mi dominio, extraño.",
|
||||||
|
site: `${context.site}es/`,
|
||||||
|
customData: [
|
||||||
|
"<language>es-mx</language>",
|
||||||
|
`<image>
|
||||||
|
<url>https://juancman.dev/logo.png</url>
|
||||||
|
<title>juancmandev</title>
|
||||||
|
<link>https://juancman.dev</link>
|
||||||
|
</image>`,
|
||||||
|
`<atom:link href="${context.site}es/feed.xml" rel="self" type="application/rss+xml"/>`,
|
||||||
|
].join(""),
|
||||||
|
items,
|
||||||
|
trailingSlash: false,
|
||||||
|
});
|
||||||
|
}
|
115
src/pages/es/index.astro
Normal file
115
src/pages/es/index.astro
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
import LinkButton from "@/components/link-button";
|
||||||
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "juancmandev",
|
||||||
|
description:
|
||||||
|
"Bienvenido a mi dominio, extraño. Soy juancmandev; Desarrollador Web, entusiasta de Linux, y defensor de la privacidad.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||||
|
const allEsPosts = allPosts.map((post) => {
|
||||||
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEsPosts);
|
||||||
|
const last3Blogs = allEsPosts.slice(0, 3);
|
||||||
|
|
||||||
|
const allProjects = await getCollection(
|
||||||
|
"portfolio",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
const allEnProjects = allProjects.map((project) => {
|
||||||
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEnProjects);
|
||||||
|
const last3Projects = allEnProjects.slice(0, 3);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout {...pageData}>
|
||||||
|
<div class="prose prose-invert">
|
||||||
|
<h1 class="text-primary">Bienvenido a mi dominio, extraño.</h1>
|
||||||
|
<p>
|
||||||
|
Soy <strong class="text-primary">juancmandev</strong>; <strong
|
||||||
|
>Desarrollador Web</strong
|
||||||
|
>, entusiasta de <strong>Linux</strong> y defensor de la <strong
|
||||||
|
>privacidad.</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Este es mi <strong>website</strong>, un pedazo de Internet al que puedo
|
||||||
|
llamar <strong>hogar</strong>. Aquí comparto mi pasión por proyectos open
|
||||||
|
source y otros temas.
|
||||||
|
</p>
|
||||||
|
<section>
|
||||||
|
<h2>Últimos posts</h2>
|
||||||
|
<ul class="mt-0 p-0 list-none">
|
||||||
|
{
|
||||||
|
last3Blogs.map(
|
||||||
|
(blogpost) =>
|
||||||
|
blogpost && (
|
||||||
|
<li class="p-0">
|
||||||
|
<PostItem
|
||||||
|
type="blog"
|
||||||
|
lang={lang}
|
||||||
|
slug={blogpost?.slug}
|
||||||
|
date={blogpost.data.date}
|
||||||
|
title={blogpost.data.title}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<LinkButton variant="secondary" href="/es/blog" className="no-underline"
|
||||||
|
>Más posts</LinkButton
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Últimos proyectos</h2>
|
||||||
|
<ul class="mt-0 p-0 list-none">
|
||||||
|
{
|
||||||
|
last3Projects.map(
|
||||||
|
(project) =>
|
||||||
|
project && (
|
||||||
|
<li class="p-0">
|
||||||
|
<PostItem
|
||||||
|
lang={lang}
|
||||||
|
type="portfolio"
|
||||||
|
slug={project.slug}
|
||||||
|
date={project.data.date!}
|
||||||
|
title={project.data.title!}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<LinkButton
|
||||||
|
variant="secondary"
|
||||||
|
href="/es/portfolio"
|
||||||
|
className="no-underline">Más proyectos</LinkButton
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
51
src/pages/es/portfolio/[...slug].astro
Normal file
51
src/pages/es/portfolio/[...slug].astro
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import components from "@/components/mdx/wrapper";
|
||||||
|
import formatDate from "@/utils/format-date";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: CollectionEntry<"portfolio">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allProjects = await getCollection(
|
||||||
|
"portfolio",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
const filterEnProjects = allProjects.map((project) => {
|
||||||
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterEnProjects.map((project) => ({
|
||||||
|
params: { slug: project?.slug },
|
||||||
|
props: { project },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
const { Content } = await project.render();
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={project.data.title} description={project.data.description}>
|
||||||
|
<article class="prose prose-invert">
|
||||||
|
<h1>{project.data.title}</h1>
|
||||||
|
<Content components={{ ...components }} />
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>Publicado: </strong>
|
||||||
|
{project.data.date && formatDate(new Date(project.data.date), lang)}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Layout>
|
55
src/pages/es/portfolio/index.astro
Normal file
55
src/pages/es/portfolio/index.astro
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "Portfolio",
|
||||||
|
description: "Revisa mis proyectos.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allProjects = await getCollection(
|
||||||
|
"portfolio",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
const allEsProjects = allProjects.map((project) => {
|
||||||
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "es")
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEsProjects);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout {...pageData}>
|
||||||
|
<section class="prose prose-invert">
|
||||||
|
<h1>{pageData.title}</h1>
|
||||||
|
<p>{pageData.description}</p>
|
||||||
|
</section>
|
||||||
|
<ul class="mt-4 flex flex-col gap-4">
|
||||||
|
{
|
||||||
|
allEsProjects.map(
|
||||||
|
(project) =>
|
||||||
|
project && (
|
||||||
|
<li>
|
||||||
|
<PostItem
|
||||||
|
lang={lang}
|
||||||
|
type="portfolio"
|
||||||
|
slug={project.slug}
|
||||||
|
date={project.data.date!}
|
||||||
|
title={project.data.title!}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Layout>
|
41
src/pages/es/videos/[...slug].astro
Normal file
41
src/pages/es/videos/[...slug].astro
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import components from "@/components/mdx/wrapper";
|
||||||
|
import formatDate from "@/utils/format-date";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: CollectionEntry<"videos">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allProjects = await getCollection(
|
||||||
|
"videos",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return allProjects.map((project) => ({
|
||||||
|
params: { slug: project.slug },
|
||||||
|
props: { project },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
const { Content } = await project.render();
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={project.data.title} description={project.data.description}>
|
||||||
|
<article class="prose prose-invert">
|
||||||
|
<h1>{project.data.title}</h1>
|
||||||
|
<Content components={{ ...components }} />
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<strong>Posted: </strong>
|
||||||
|
{project.data.date && formatDate(new Date(project.data.date), lang)}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Layout>
|
42
src/pages/es/videos/index.astro
Normal file
42
src/pages/es/videos/index.astro
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "Videos",
|
||||||
|
description: "Guiones de los videos de mi canal de YouTube.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allVideos = await getCollection(
|
||||||
|
"videos",
|
||||||
|
({ data }) => data.draft !== true,
|
||||||
|
);
|
||||||
|
sortContentByDate(allVideos);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout {...pageData}>
|
||||||
|
<section class="prose prose-invert">
|
||||||
|
<h1>{pageData.title}</h1>
|
||||||
|
<p>{pageData.description}</p>
|
||||||
|
</section>
|
||||||
|
<ul class="mt-4 flex flex-col gap-4">
|
||||||
|
{
|
||||||
|
allVideos.map((video: any) => (
|
||||||
|
<li>
|
||||||
|
<PostItem
|
||||||
|
lang={lang}
|
||||||
|
type="es/videos"
|
||||||
|
slug={video.slug}
|
||||||
|
date={video.data.date!}
|
||||||
|
title={video.data.title!}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Layout>
|
@ -10,10 +10,10 @@ import type { ImageMetadata } from "astro";
|
|||||||
const markdownParser = new MarkdownIt();
|
const markdownParser = new MarkdownIt();
|
||||||
|
|
||||||
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
|
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
|
||||||
"/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}",
|
"/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}"
|
||||||
);
|
);
|
||||||
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
|
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
|
||||||
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}",
|
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}"
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function GET(context: any) {
|
export async function GET(context: any) {
|
||||||
@ -21,14 +21,25 @@ export async function GET(context: any) {
|
|||||||
|
|
||||||
const blog = await getCollection(
|
const blog = await getCollection(
|
||||||
"blog",
|
"blog",
|
||||||
({ data }) => data.draft !== true && data.rss === true,
|
({ data }) => data.draft !== true && data.rss === true
|
||||||
);
|
);
|
||||||
|
const filterBlog = blog.filter((post) => {
|
||||||
|
const [lang] = post.slug.split("/");
|
||||||
|
|
||||||
|
return lang !== "es" && post;
|
||||||
|
});
|
||||||
|
|
||||||
const portfolio = await getCollection(
|
const portfolio = await getCollection(
|
||||||
"portfolio",
|
"portfolio",
|
||||||
({ data }) => data.draft !== true && data.rss === true,
|
({ data }) => data.draft !== true && data.rss === true
|
||||||
);
|
);
|
||||||
|
const filterPortfolio = portfolio.filter((project) => {
|
||||||
|
const [lang] = project.slug.split("/");
|
||||||
|
|
||||||
for await (const post of blog) {
|
return lang !== "es" && project;
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const post of filterBlog) {
|
||||||
const body = markdownParser.render(post.body);
|
const body = markdownParser.render(post.body);
|
||||||
const html = htmlParser.parse(body);
|
const html = htmlParser.parse(body);
|
||||||
const images = html.querySelectorAll("img");
|
const images = html.querySelectorAll("img");
|
||||||
@ -40,14 +51,14 @@ export async function GET(context: any) {
|
|||||||
const prefixRemoved = src.replace("@/", "");
|
const prefixRemoved = src.replace("@/", "");
|
||||||
const imagePathPrefix = `/src/${prefixRemoved}`;
|
const imagePathPrefix = `/src/${prefixRemoved}`;
|
||||||
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
|
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
|
||||||
(res: any) => res.default,
|
(res: any) => res.default
|
||||||
);
|
);
|
||||||
|
|
||||||
if (imagePath) {
|
if (imagePath) {
|
||||||
const optimizedImg = await getImage({ src: imagePath });
|
const optimizedImg = await getImage({ src: imagePath });
|
||||||
img.setAttribute(
|
img.setAttribute(
|
||||||
"src",
|
"src",
|
||||||
context.site + optimizedImg.src.replace("/", ""),
|
context.site + optimizedImg.src.replace("/", "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (src.startsWith("/images")) {
|
} else if (src.startsWith("/images")) {
|
||||||
@ -68,7 +79,7 @@ export async function GET(context: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const project of portfolio) {
|
for await (const project of filterPortfolio) {
|
||||||
const body = markdownParser.render(project.body);
|
const body = markdownParser.render(project.body);
|
||||||
const html = htmlParser.parse(body);
|
const html = htmlParser.parse(body);
|
||||||
const images = html.querySelectorAll("img");
|
const images = html.querySelectorAll("img");
|
||||||
@ -80,14 +91,14 @@ export async function GET(context: any) {
|
|||||||
const prefixRemoved = src.replace("@/", "");
|
const prefixRemoved = src.replace("@/", "");
|
||||||
const imagePathPrefix = `/src/${prefixRemoved}`;
|
const imagePathPrefix = `/src/${prefixRemoved}`;
|
||||||
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
|
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
|
||||||
(res: any) => res.default,
|
(res: any) => res.default
|
||||||
);
|
);
|
||||||
|
|
||||||
if (imagePath) {
|
if (imagePath) {
|
||||||
const optimizedImg = await getImage({ src: imagePath });
|
const optimizedImg = await getImage({ src: imagePath });
|
||||||
img.setAttribute(
|
img.setAttribute(
|
||||||
"src",
|
"src",
|
||||||
context.site + optimizedImg.src.replace("/", ""),
|
context.site + optimizedImg.src.replace("/", "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (src.startsWith("/images")) {
|
} else if (src.startsWith("/images")) {
|
||||||
@ -121,7 +132,7 @@ export async function GET(context: any) {
|
|||||||
<title>juancmandev</title>
|
<title>juancmandev</title>
|
||||||
<link>https://juancman.dev</link>
|
<link>https://juancman.dev</link>
|
||||||
</image>`,
|
</image>`,
|
||||||
`<atom:link href="${context.site}rss.xml" rel="self" type="application/rss+xml"/>`,
|
`<atom:link href="${context.site}feed.xml" rel="self" type="application/rss+xml"/>`,
|
||||||
].join(""),
|
].join(""),
|
||||||
items,
|
items,
|
||||||
trailingSlash: false,
|
trailingSlash: false,
|
@ -4,78 +4,105 @@ import LinkButton from "@/components/link-button";
|
|||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import PostItem from "@/components/post-item";
|
import PostItem from "@/components/post-item";
|
||||||
import { sortContentByDate } from "@/utils/sorts";
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "juancmandev",
|
||||||
|
description:
|
||||||
|
"Welcome to my domain, stranger. I am juancmandev; Web Developer, Linux enthusiast, and privacy defender.",
|
||||||
|
};
|
||||||
|
|
||||||
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||||
sortContentByDate(allPosts);
|
const allEnPosts = allPosts.map((post) => {
|
||||||
const last3Blogs = allPosts.slice(0, 3);
|
const [lang, ...slug] = post.slug.split("/");
|
||||||
|
|
||||||
|
if (lang !== "es")
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEnPosts);
|
||||||
|
const last3Blogs = allEnPosts.slice(0, 3);
|
||||||
|
|
||||||
const allProjects = await getCollection(
|
const allProjects = await getCollection(
|
||||||
"portfolio",
|
"portfolio",
|
||||||
({ data }) => data.draft !== true,
|
({ data }) => data.draft !== true,
|
||||||
);
|
);
|
||||||
sortContentByDate(allProjects);
|
const allEnProjects = allProjects.map((project) => {
|
||||||
const last3Projects = allProjects.slice(0, 3);
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
|
if (lang !== "es")
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEnProjects);
|
||||||
|
const last3Projects = allEnProjects.slice(0, 3);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout {...pageData}>
|
||||||
title="juancmandev"
|
<div class="prose prose-invert">
|
||||||
description="Welcome to my domain, stranger. I am juancmandev; Web Developer, Linux enthusiast, and privacy defender."
|
<h1 class="text-primary">Welcome to my domain, stranger.</h1>
|
||||||
>
|
<p>
|
||||||
<div class="prose prose-invert">
|
I am <strong class="text-primary">juancmandev</strong>; <strong
|
||||||
<h1 class="text-primary">Welcome to my domain, stranger.</h1>
|
>Web Developer</strong
|
||||||
<p>
|
>, <strong>Linux</strong> enthusiast, and <strong>privacy</strong> defender.
|
||||||
I am <strong class="text-primary">juancmandev</strong>; <strong
|
</p>
|
||||||
>Web Developer</strong
|
<p>
|
||||||
>, <strong>Linux</strong> enthusiast, and <strong>privacy</strong> defender.
|
This is my <strong>website</strong>, a piece of the Internet that I could
|
||||||
</p>
|
call my <strong>home base</strong>. Here, I share my passion about open
|
||||||
<p>
|
source projects and other topics.
|
||||||
This is my <strong>website</strong>, a piece of the Internet that I
|
</p>
|
||||||
could call my <strong>home base</strong>. Here, I share my knowledge
|
<section>
|
||||||
about my career and talk about other topics.
|
<h2>Latest posts</h2>
|
||||||
</p>
|
<ul class="mt-0 p-0 list-none">
|
||||||
<section>
|
{
|
||||||
<h2>Latest posts</h2>
|
last3Blogs.map((blogpost: any) => (
|
||||||
<ul class="mt-0 p-0 list-none">
|
<li class="p-0">
|
||||||
{
|
<PostItem
|
||||||
last3Blogs.map((blogpost: any) => (
|
type="blog"
|
||||||
<li class="p-0">
|
lang={lang}
|
||||||
<PostItem
|
slug={blogpost.slug}
|
||||||
type="blog"
|
date={blogpost.data.date!}
|
||||||
slug={blogpost.slug}
|
title={blogpost.data.title!}
|
||||||
date={blogpost.data.date!}
|
/>
|
||||||
title={blogpost.data.title!}
|
</li>
|
||||||
/>
|
))
|
||||||
</li>
|
}
|
||||||
))
|
</ul>
|
||||||
}
|
<LinkButton variant="secondary" href="/blog" className="no-underline"
|
||||||
</ul>
|
>More posts</LinkButton
|
||||||
<LinkButton
|
>
|
||||||
variant="secondary"
|
</section>
|
||||||
href="/blog"
|
<section>
|
||||||
className="no-underline">More posts</LinkButton
|
<h2>Latest projects</h2>
|
||||||
>
|
<ul class="mt-0 p-0 list-none">
|
||||||
</section>
|
{
|
||||||
<section>
|
last3Projects.map(
|
||||||
<h2>Latest projects</h2>
|
(project) =>
|
||||||
<ul class="mt-0 p-0 list-none">
|
project && (
|
||||||
{
|
<li class="p-0">
|
||||||
last3Projects.map((project: any) => (
|
<PostItem
|
||||||
<li class="p-0">
|
lang={lang}
|
||||||
<PostItem
|
type="portfolio"
|
||||||
type="portfolio"
|
slug={project.slug}
|
||||||
slug={project.slug}
|
date={project.data.date!}
|
||||||
date={project.data.date!}
|
title={project.data.title!}
|
||||||
title={project.data.title!}
|
/>
|
||||||
/>
|
</li>
|
||||||
</li>
|
),
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<LinkButton
|
<LinkButton variant="secondary" href="/portfolio" className="no-underline"
|
||||||
variant="secondary"
|
>More projects</LinkButton
|
||||||
href="/portfolio"
|
>
|
||||||
className="no-underline">More projects</LinkButton
|
</section>
|
||||||
>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -5,27 +5,27 @@ import { createServerClient } from "@/utils/pocketbase";
|
|||||||
|
|
||||||
const pb = createServerClient(import.meta.env.SECRET_POCKETBASE_API_URL);
|
const pb = createServerClient(import.meta.env.SECRET_POCKETBASE_API_URL);
|
||||||
const data = await pb.collection("microblogs").getFullList({
|
const data = await pb.collection("microblogs").getFullList({
|
||||||
expand: "tags",
|
expand: "tags",
|
||||||
sort: "-published",
|
sort: "-published",
|
||||||
});
|
});
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title="Microblog"
|
title="Microblog"
|
||||||
description="Short-format writing. Instead of using shitty social media."
|
description="Short-format writing. Instead of using shitty social media."
|
||||||
>
|
>
|
||||||
<div class="prose prose-invert">
|
<div class="prose prose-invert">
|
||||||
<h1>Microblog</h1>
|
<h1>Microblog</h1>
|
||||||
<p>Short-format writing.</p>
|
<p>Short-format writing.</p>
|
||||||
<p>Instead of using shitty social media.</p>
|
<p>Instead of using shitty social media.</p>
|
||||||
<ul class="mx-auto p-0 mt-10 flex flex-col gap-10 list-none">
|
<ul class="mx-auto p-0 mt-10 flex flex-col gap-10 list-none">
|
||||||
{
|
{
|
||||||
data.map((item: any) => (
|
data.map((item: any) => (
|
||||||
<li>
|
<li>
|
||||||
<MicroblogItem {...item} />
|
<MicroblogItem {...item} />
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
import components from "@/components/mdx/wrapper";
|
import components from "@/components/mdx/wrapper";
|
||||||
import formatDate from "@/utils/format-date";
|
import formatDate from "@/utils/format-date";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: CollectionEntry<"portfolio">;
|
project: CollectionEntry<"portfolio">;
|
||||||
@ -14,15 +15,27 @@ export async function getStaticPaths() {
|
|||||||
"portfolio",
|
"portfolio",
|
||||||
({ data }) => data.draft !== true,
|
({ data }) => data.draft !== true,
|
||||||
);
|
);
|
||||||
|
const filterEnProjects = allProjects.map((project) => {
|
||||||
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
return allProjects.map((project) => ({
|
if (lang === "en")
|
||||||
params: { slug: project.slug },
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filterEnProjects.map((project) => ({
|
||||||
|
params: { slug: project?.slug },
|
||||||
props: { project },
|
props: { project },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { project } = Astro.props;
|
const { project } = Astro.props;
|
||||||
const { Content } = await project.render();
|
const { Content } = await project.render();
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={project.data.title} description={project.data.description}>
|
<Layout title={project.data.title} description={project.data.description}>
|
||||||
@ -32,7 +45,7 @@ const { Content } = await project.render();
|
|||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
<strong>Posted: </strong>
|
<strong>Posted: </strong>
|
||||||
{project.data.date && formatDate(new Date(project.data.date))}
|
{project.data.date && formatDate(new Date(project.data.date), lang)}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,33 +1,55 @@
|
|||||||
---
|
---
|
||||||
import PostItem from "@/components/post-item";
|
import PostItem from "@/components/post-item";
|
||||||
|
import { getLangFromUrl } from "@/i18n/utils";
|
||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import { sortContentByDate } from "@/utils/sorts";
|
import { sortContentByDate } from "@/utils/sorts";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
const pageData = {
|
||||||
|
title: "Portfolio",
|
||||||
|
description: "Check my projects.",
|
||||||
|
};
|
||||||
|
|
||||||
const allProjects = await getCollection(
|
const allProjects = await getCollection(
|
||||||
"portfolio",
|
"portfolio",
|
||||||
({ data }) => data.draft !== true,
|
({ data }) => data.draft !== true,
|
||||||
);
|
);
|
||||||
sortContentByDate(allProjects);
|
const allEnProjects = allProjects.map((project) => {
|
||||||
|
const [lang, ...slug] = project.slug.split("/");
|
||||||
|
|
||||||
|
if (lang === "en")
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
slug: slug.toString(),
|
||||||
|
};
|
||||||
|
else null;
|
||||||
|
});
|
||||||
|
sortContentByDate(allEnProjects);
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Blog" description="Long format about thoughts and other topics.">
|
<Layout {...pageData}>
|
||||||
<section class="prose prose-invert">
|
<section class="prose prose-invert">
|
||||||
<h1>Portfolio</h1>
|
<h1>{pageData.title}</h1>
|
||||||
<p>Check my projects.</p>
|
<p>{pageData.description}</p>
|
||||||
</section>
|
</section>
|
||||||
<ul class="mt-4 flex flex-col gap-4">
|
<ul class="mt-4 flex flex-col gap-4">
|
||||||
{
|
{
|
||||||
allProjects.map((project: any) => (
|
allEnProjects.map(
|
||||||
<li>
|
(project) =>
|
||||||
<PostItem
|
project && (
|
||||||
type="portfolio"
|
<li>
|
||||||
slug={project.slug}
|
<PostItem
|
||||||
date={project.data.date!}
|
lang={lang}
|
||||||
title={project.data.title!}
|
type="portfolio"
|
||||||
/>
|
slug={project.slug}
|
||||||
</li>
|
date={project.data.date!}
|
||||||
))
|
title={project.data.title!}
|
||||||
}
|
/>
|
||||||
</ul>
|
</li>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -12,12 +12,29 @@ const months = [
|
|||||||
"November",
|
"November",
|
||||||
"December",
|
"December",
|
||||||
];
|
];
|
||||||
|
const meses = [
|
||||||
|
"Enero",
|
||||||
|
"Febrero",
|
||||||
|
"Marzo",
|
||||||
|
"Abril",
|
||||||
|
"Mayo",
|
||||||
|
"Junio",
|
||||||
|
"Julio",
|
||||||
|
"Agosto",
|
||||||
|
"Septiembre",
|
||||||
|
"Octubre",
|
||||||
|
"Noviembre",
|
||||||
|
"Diciembre",
|
||||||
|
];
|
||||||
|
|
||||||
export default function formatDate(date: Date | string) {
|
export default function formatDate(date: Date | string, lang: string) {
|
||||||
const newDate = new Date(date);
|
const newDate = new Date(date);
|
||||||
const month = months[newDate.getMonth()];
|
const month = months[newDate.getMonth()];
|
||||||
|
const mes = meses[newDate.getMonth()];
|
||||||
const day = newDate.getDate();
|
const day = newDate.getDate();
|
||||||
const year = newDate.getFullYear();
|
const year = newDate.getFullYear();
|
||||||
|
|
||||||
return `${month} ${day}, ${year}`;
|
return lang !== "es"
|
||||||
|
? `${month} ${day}, ${year}`
|
||||||
|
: `${day} de ${mes} del ${year}`;
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
type TNavItem = {
|
|
||||||
label: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const navItems: TNavItem[] = [
|
|
||||||
{
|
|
||||||
label: "Blog",
|
|
||||||
to: "/blog",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Portfolio",
|
|
||||||
to: "/portfolio",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Microblog",
|
|
||||||
to: "/microblog",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Resources",
|
|
||||||
to: "/resources",
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// label: "Videos",
|
|
||||||
// to: "/videos",
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: "About",
|
|
||||||
to: "/about",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Contact",
|
|
||||||
to: "/contact",
|
|
||||||
},
|
|
||||||
];
|
|
@ -6,6 +6,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user