migrate website to astro
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
.vercel
|
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
54
README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Astro Starter Kit: Basics
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||

|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ └── Card.astro
|
||||
│ ├── layouts/
|
||||
│ │ └── Layout.astro
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
32
astro.config.mjs
Normal file
@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import react from "@astrojs/react";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import vercel from "@astrojs/vercel/serverless";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "hybrid",
|
||||
adapter: vercel(),
|
||||
site: "https://www.juancman.dev/",
|
||||
integrations: [
|
||||
react(),
|
||||
tailwind({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
mdx({
|
||||
syntaxHighlight: false,
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: "catppuccin-mocha",
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
17
components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.mjs",
|
||||
"css": "./src/styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
45
package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "website",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.7.0",
|
||||
"@astrojs/mdx": "^3.1.1",
|
||||
"@astrojs/react": "^3.6.0",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@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",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"astro": "^4.11.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"lucide-react": "^0.396.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rehype-pretty-code": "^0.13.2",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pocketbase": "^0.21.3"
|
||||
}
|
||||
}
|
6497
pnpm-lock.yaml
generated
Normal file
3179
public/favicon.svg
Normal file
After Width: | Height: | Size: 182 KiB |
30
src/assets/about/written-by-human-not-by-ai.svg
Normal file
@ -0,0 +1,30 @@
|
||||
<svg width="131" height="42" viewBox="0 0 131 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 0.5H116C124.008 0.5 130.5 6.99187 130.5 15V41.5H15C6.99187 41.5 0.5 35.0081 0.5 27V0.5Z" fill="white" stroke="black"/>
|
||||
<path d="M17.9605 24.1575C21.4266 26.9643 26.3836 26.9643 29.8497 24.1575L28.5095 22.5026C25.8248 24.6766 21.9854 24.6766 19.3007 22.5026L17.9605 24.1575Z" fill="black"/>
|
||||
<path d="M19.404 20.5134V17.6365H21.5336V20.5134H19.404Z" fill="black"/>
|
||||
<path d="M26.012 17.6365V20.5134H28.1415V17.6365H26.012Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M35 21.5C35 27.8513 29.8513 33 23.5 33C17.1487 33 12 27.8513 12 21.5C12 15.1487 17.1487 10 23.5 10C29.8513 10 35 15.1487 35 21.5ZM32.8705 21.5C32.8705 26.6752 28.6752 30.8705 23.5 30.8705C18.3248 30.8705 14.1295 26.6752 14.1295 21.5C14.1295 16.3248 18.3248 12.1295 23.5 12.1295C28.6752 12.1295 32.8705 16.3248 32.8705 21.5Z" fill="black"/>
|
||||
<path d="M48.2896 22.1781C49.2796 22.1781 50.088 22.4414 50.7148 22.9681C51.3474 23.4889 51.6638 24.356 51.6638 25.5692V32.0851H49.098V26.1995C49.098 25.6905 49.0307 25.2999 48.8959 25.0277C48.6499 24.5305 48.1813 24.282 47.49 24.282C46.6407 24.282 46.0578 24.646 45.7415 25.3739C45.5775 25.7586 45.4954 26.2498 45.4954 26.8475V32.0851H43V22.4266H45.4164V23.8381C45.7385 23.341 46.0432 22.9829 46.3302 22.764C46.8457 22.3734 47.4988 22.1781 48.2896 22.1781Z" fill="black"/>
|
||||
<path d="M57.5604 30.3008C58.2926 30.3008 58.855 30.0374 59.2475 29.5107C59.6399 28.984 59.8362 28.2353 59.8362 27.2648C59.8362 26.2942 59.6399 25.5485 59.2475 25.0277C58.855 24.501 58.2926 24.2376 57.5604 24.2376C56.8282 24.2376 56.2629 24.501 55.8646 25.0277C55.4721 25.5485 55.2758 26.2942 55.2758 27.2648C55.2758 28.2353 55.4721 28.984 55.8646 29.5107C56.2629 30.0374 56.8282 30.3008 57.5604 30.3008ZM62.4634 27.2648C62.4634 28.6851 62.0592 29.9013 61.2509 30.9133C60.4425 31.9194 59.2152 32.4225 57.5692 32.4225C55.9231 32.4225 54.6959 31.9194 53.8875 30.9133C53.0791 29.9013 52.675 28.6851 52.675 27.2648C52.675 25.8681 53.0791 24.6578 53.8875 23.6339C54.6959 22.6101 55.9231 22.0982 57.5692 22.0982C59.2152 22.0982 60.4425 22.6101 61.2509 23.6339C62.0592 24.6578 62.4634 25.8681 62.4634 27.2648Z" fill="black"/>
|
||||
<path d="M62.5608 24.2997V22.4977H63.8964V19.799H66.3742V22.4977H67.9295V24.2997H66.3742V29.4131C66.3742 29.8096 66.424 30.0581 66.5236 30.1587C66.6232 30.2534 66.9278 30.3008 67.4374 30.3008C67.5136 30.3008 67.5927 30.3008 67.6747 30.3008C67.7626 30.2949 67.8475 30.2889 67.9295 30.283V32.1739L66.7433 32.2183C65.56 32.2597 64.7516 32.0526 64.3181 31.5969C64.037 31.3069 63.8964 30.8601 63.8964 30.2564V24.2997H62.5608Z" fill="black"/>
|
||||
<path d="M77.602 22.1958C78.8615 22.1958 79.8456 22.6545 80.5544 23.5718C81.2691 24.4891 81.6264 25.6728 81.6264 27.1227C81.6264 28.6259 81.2749 29.8717 80.572 30.8601C79.869 31.8484 78.8878 32.3426 77.6284 32.3426C76.8376 32.3426 76.202 32.1828 75.7217 31.8632C75.4346 31.6738 75.1242 31.3424 74.7903 30.8689V32.0851H72.3388V19.0178H74.8342V23.6695C75.1505 23.2197 75.4991 22.8764 75.8798 22.6397C76.3309 22.3438 76.905 22.1958 77.602 22.1958ZM76.9606 30.2564C77.605 30.2564 78.1058 29.993 78.4631 29.4663C78.8205 28.9396 78.9991 28.2472 78.9991 27.389C78.9991 26.7025 78.9113 26.1344 78.7355 25.6846C78.4016 24.8324 77.7866 24.4063 76.8903 24.4063C75.9823 24.4063 75.3585 24.8235 75.0187 25.658C74.843 26.1018 74.7551 26.6759 74.7551 27.3802C74.7551 28.2087 74.9367 28.8952 75.2999 29.4397C75.6631 29.9842 76.2167 30.2564 76.9606 30.2564Z" fill="black"/>
|
||||
<path d="M83.0945 33.9405L83.4108 33.9582C83.6568 33.9701 83.8912 33.9612 84.1137 33.9316C84.3363 33.902 84.5238 33.8339 84.6761 33.7274C84.8225 33.6268 84.9573 33.4167 85.0803 33.0971C85.2092 32.7775 85.2619 32.5822 85.2385 32.5112L81.7237 22.4089H84.5092L86.6004 29.5462L88.5774 22.4089H91.2398L87.9536 31.9253C87.3209 33.76 86.8201 34.8963 86.451 35.3342C86.082 35.7781 85.3439 36 84.2368 36C84.0142 36 83.8355 35.997 83.7008 35.9911C83.566 35.9911 83.3639 35.9822 83.0945 35.9645V33.9405Z" fill="black"/>
|
||||
<path d="M97.783 27.1405H101.069L99.4525 21.9916L97.783 27.1405ZM97.95 19H101.008L105.594 32.0851H102.66L101.825 29.3953H97.0537L96.1575 32.0851H93.3281L97.95 19Z" fill="black"/>
|
||||
<path d="M110.59 32.0851H107.902V19H110.59V32.0851Z" fill="black"/>
|
||||
<path d="M106.306 19H112V21.2258H106.306V19Z" fill="black"/>
|
||||
<path d="M106.306 29.8624H112V32.0882H106.306V29.8624Z" fill="black"/>
|
||||
<path d="M42.9754 9.89597L43.9953 13.8667L45.0301 9.89597H46.0303L47.07 13.8432L48.1544 9.89597H49.0456L47.5058 14.9347H46.5799L45.5005 11.0345L44.4558 14.9347H43.5299L42 9.89597H42.9754Z" fill="black"/>
|
||||
<path d="M49.8156 9.89597H50.6622V10.7663C50.7316 10.597 50.9016 10.3915 51.1722 10.15C51.4429 9.90538 51.7548 9.78306 52.108 9.78306C52.1245 9.78306 52.1526 9.78463 52.1922 9.78777C52.2318 9.7909 52.2994 9.79718 52.3952 9.80659V10.7005C52.3424 10.6911 52.2928 10.6848 52.2466 10.6817C52.2037 10.6785 52.1559 10.6769 52.1031 10.6769C51.6541 10.6769 51.3092 10.815 51.0682 11.091C50.8273 11.3638 50.7068 11.679 50.7068 12.0366V14.9347H49.8156V9.89597Z" fill="black"/>
|
||||
<path d="M53.0662 9.9195H53.9722V14.9347H53.0662V9.9195ZM53.0662 8.02352H53.9722V8.98327H53.0662V8.02352Z" fill="black"/>
|
||||
<path d="M55.4008 8.48928H56.3019V9.89597H57.1485V10.5876H56.3019V13.8761C56.3019 14.0518 56.3646 14.1694 56.49 14.229C56.5593 14.2635 56.6749 14.2807 56.8366 14.2807C56.8795 14.2807 56.9257 14.2807 56.9752 14.2807C57.0248 14.2776 57.0825 14.2729 57.1485 14.2666V14.9347C57.0462 14.9629 56.9389 14.9833 56.8267 14.9958C56.7178 15.0084 56.599 15.0146 56.4702 15.0146C56.0543 15.0146 55.7721 14.9143 55.6236 14.7135C55.475 14.5097 55.4008 14.2462 55.4008 13.9232V10.5876H54.6828V9.89597H55.4008V8.48928Z" fill="black"/>
|
||||
<path d="M58.1215 8.48928H59.0227V9.89597H59.8693V10.5876H59.0227V13.8761C59.0227 14.0518 59.0854 14.1694 59.2108 14.229C59.2801 14.2635 59.3957 14.2807 59.5574 14.2807C59.6003 14.2807 59.6465 14.2807 59.696 14.2807C59.7455 14.2776 59.8033 14.2729 59.8693 14.2666V14.9347C59.767 14.9629 59.6597 14.9833 59.5475 14.9958C59.4386 15.0084 59.3197 15.0146 59.191 15.0146C58.7751 15.0146 58.4929 14.9143 58.3444 14.7135C58.1958 14.5097 58.1215 14.2462 58.1215 13.9232V10.5876H57.4036V9.89597H58.1215V8.48928Z" fill="black"/>
|
||||
<path d="M62.8723 9.78306C63.2486 9.78306 63.6134 9.86775 63.9666 10.0371C64.3197 10.2033 64.5888 10.4198 64.7736 10.6864C64.9518 10.9404 65.0707 11.2368 65.1301 11.5755C65.1829 11.8076 65.2093 12.1777 65.2093 12.6858H61.3226C61.3391 13.1971 61.4662 13.6079 61.7039 13.9185C61.9415 14.2258 62.3095 14.3795 62.808 14.3795C63.2734 14.3795 63.6447 14.2337 63.922 13.942C64.0804 13.7726 64.1927 13.5766 64.2587 13.3539H65.135C65.1119 13.5389 65.0344 13.7459 64.9023 13.9749C64.7736 14.2007 64.6284 14.3858 64.4666 14.5301C64.196 14.781 63.8609 14.9503 63.4615 15.0382C63.247 15.0883 63.0044 15.1134 62.7337 15.1134C62.0735 15.1134 61.5141 14.886 61.0552 14.4313C60.5964 13.9733 60.367 13.3335 60.367 12.5118C60.367 11.7026 60.5981 11.0455 61.0602 10.5405C61.5223 10.0355 62.1264 9.78306 62.8723 9.78306ZM64.2933 12.0131C64.257 11.6461 64.1729 11.3528 64.0408 11.1333C63.7966 10.7256 63.3889 10.5217 62.8179 10.5217C62.4086 10.5217 62.0653 10.6628 61.788 10.9451C61.5108 11.2243 61.3639 11.5802 61.3474 12.0131H64.2933Z" fill="black"/>
|
||||
<path d="M66.2071 9.89597H67.0537V10.6111C67.3046 10.3163 67.5703 10.1045 67.8509 9.97595C68.1315 9.84736 68.4434 9.78306 68.7867 9.78306C69.5392 9.78306 70.0476 10.0324 70.3116 10.5311C70.4569 10.804 70.5295 11.1945 70.5295 11.7026V14.9347H69.6234V11.759C69.6234 11.4516 69.5756 11.2039 69.4798 11.0157C69.3214 10.702 69.0342 10.5452 68.6183 10.5452C68.4071 10.5452 68.2338 10.5656 68.0984 10.6064C67.8542 10.6754 67.6396 10.8134 67.4548 11.0204C67.3062 11.1866 67.2089 11.3591 67.1627 11.5379C67.1198 11.7135 67.0983 11.966 67.0983 12.2953V14.9347H66.2071V9.89597Z" fill="black"/>
|
||||
<path d="M74.4015 8H75.268V10.5076C75.4628 10.2661 75.6955 10.0826 75.9661 9.95714C76.2368 9.82854 76.5306 9.76424 76.8474 9.76424C77.5076 9.76424 78.0423 9.98066 78.4516 10.4135C78.8642 10.8432 79.0705 11.4783 79.0705 12.3189C79.0705 13.1155 78.8675 13.7773 78.4615 14.3042C78.0555 14.8312 77.4927 15.0946 76.7732 15.0946C76.3705 15.0946 76.0305 15.0021 75.7532 14.817C75.5882 14.7073 75.4116 14.5316 75.2234 14.2901V14.9347H74.4015V8ZM76.7187 14.3466C77.2006 14.3466 77.5604 14.1647 77.7981 13.8008C78.039 13.437 78.1595 12.9571 78.1595 12.3612C78.1595 11.8312 78.039 11.3921 77.7981 11.0439C77.5604 10.6958 77.2089 10.5217 76.7435 10.5217C76.3375 10.5217 75.981 10.6644 75.674 10.9498C75.3703 11.2352 75.2185 11.7057 75.2185 12.3612C75.2185 12.8348 75.2812 13.219 75.4066 13.5139C75.641 14.069 76.0784 14.3466 76.7187 14.3466Z" fill="black"/>
|
||||
<path d="M83.3262 9.89597H84.3115C84.1861 10.219 83.9071 10.9561 83.4747 12.1072C83.1513 12.9728 82.8806 13.6785 82.6627 14.2243C82.1478 15.5102 81.7847 16.2943 81.5735 16.5766C81.3622 16.8589 80.9991 17 80.4842 17C80.3588 17 80.2614 16.9953 80.1921 16.9859C80.1261 16.9765 80.0435 16.9592 79.9445 16.9341V16.1626C80.0997 16.2033 80.2119 16.2284 80.2812 16.2378C80.3505 16.2473 80.4116 16.252 80.4644 16.252C80.6294 16.252 80.7499 16.2253 80.8258 16.172C80.9051 16.1218 80.9711 16.0591 81.0239 15.9838C81.0404 15.9587 81.0998 15.8301 81.2021 15.598C81.3045 15.3659 81.3787 15.1934 81.4249 15.0805L79.4643 9.89597H80.4743L81.8953 13.9984L83.3262 9.89597Z" fill="black"/>
|
||||
<path d="M87.7033 8H88.5945V10.5781C88.8057 10.3241 88.9955 10.1453 89.1639 10.0418C89.4511 9.86304 89.8092 9.77365 90.2383 9.77365C91.0074 9.77365 91.5289 10.0293 91.8029 10.5405C91.9514 10.8197 92.0257 11.207 92.0257 11.7026V14.9347H91.1097V11.759C91.1097 11.3889 91.0602 11.1176 90.9612 10.9451C90.7994 10.6691 90.4958 10.5311 90.0502 10.5311C89.6805 10.5311 89.3454 10.6519 89.0451 10.8934C88.7447 11.1349 88.5945 11.5912 88.5945 12.2624V14.9347H87.7033V8Z" fill="black"/>
|
||||
<path d="M94.1375 9.89597V13.241C94.1375 13.4982 94.1804 13.7083 94.2662 13.8714C94.4246 14.1725 94.7201 14.3231 95.1525 14.3231C95.773 14.3231 96.1955 14.0596 96.42 13.5327C96.5421 13.2504 96.6032 12.863 96.6032 12.3706V9.89597H97.4944V14.9347H96.6527L96.6626 14.1913C96.5471 14.3826 96.4035 14.5442 96.2318 14.6759C95.8919 14.9394 95.4793 15.0711 94.994 15.0711C94.2381 15.0711 93.7232 14.8312 93.4493 14.3513C93.3007 14.0941 93.2265 13.7507 93.2265 13.321V9.89597H94.1375Z" fill="black"/>
|
||||
<path d="M98.7892 9.89597H99.6706V10.6111C99.8818 10.3633 100.073 10.183 100.245 10.07C100.539 9.87872 100.872 9.78306 101.245 9.78306C101.668 9.78306 102.008 9.88186 102.265 10.0795C102.41 10.1924 102.542 10.3586 102.661 10.5781C102.859 10.3084 103.092 10.1093 103.359 9.98066C103.627 9.84893 103.927 9.78306 104.26 9.78306C104.973 9.78306 105.459 10.0277 105.716 10.517C105.855 10.7804 105.924 11.1349 105.924 11.5802V14.9347H104.998V11.4344C104.998 11.0988 104.909 10.8683 104.731 10.7428C104.556 10.6174 104.341 10.5546 104.087 10.5546C103.737 10.5546 103.435 10.666 103.181 10.8887C102.93 11.1113 102.805 11.483 102.805 12.0037V14.9347H101.899V11.6461C101.899 11.3042 101.856 11.0549 101.77 10.8981C101.635 10.6628 101.382 10.5452 101.012 10.5452C100.676 10.5452 100.369 10.6691 100.091 10.9169C99.8174 11.1647 99.6805 11.6132 99.6805 12.2624V14.9347H98.7892V9.89597Z" fill="black"/>
|
||||
<path d="M107.818 13.5938C107.818 13.8385 107.912 14.0314 108.1 14.1725C108.288 14.3136 108.511 14.3842 108.769 14.3842C109.082 14.3842 109.386 14.3152 109.68 14.1772C110.175 13.9482 110.422 13.5734 110.422 13.0528V12.3706C110.313 12.4365 110.173 12.4914 110.001 12.5353C109.83 12.5792 109.661 12.6106 109.496 12.6294L108.957 12.6952C108.633 12.736 108.391 12.8003 108.229 12.8881C107.955 13.0355 107.818 13.2708 107.818 13.5938ZM109.977 11.8813C110.181 11.8562 110.318 11.7747 110.388 11.6367C110.427 11.5614 110.447 11.4532 110.447 11.3121C110.447 11.0235 110.338 10.815 110.12 10.6864C109.906 10.5546 109.597 10.4888 109.194 10.4888C108.729 10.4888 108.399 10.6079 108.204 10.8463C108.095 10.978 108.024 11.1741 107.991 11.4344H107.159C107.176 10.8134 107.387 10.3821 107.793 10.1406C108.202 9.89598 108.676 9.77365 109.214 9.77365C109.838 9.77365 110.345 9.88657 110.734 10.1124C111.12 10.3382 111.313 10.6895 111.313 11.1662V14.069C111.313 14.1568 111.332 14.2274 111.368 14.2807C111.408 14.334 111.488 14.3607 111.611 14.3607C111.65 14.3607 111.695 14.3591 111.744 14.356C111.794 14.3497 111.847 14.3419 111.903 14.3325V14.9582C111.764 14.9958 111.658 15.0193 111.586 15.0288C111.513 15.0382 111.414 15.0429 111.289 15.0429C110.982 15.0429 110.759 14.9394 110.62 14.7324C110.548 14.6226 110.496 14.4673 110.467 14.2666C110.285 14.4924 110.024 14.6884 109.684 14.8547C109.345 15.0209 108.97 15.104 108.561 15.104C108.069 15.104 107.666 14.9629 107.352 14.6806C107.042 14.3952 106.887 14.0392 106.887 13.6127C106.887 13.1453 107.041 12.7831 107.348 12.5259C107.654 12.2687 108.057 12.1103 108.556 12.0507L109.977 11.8813Z" fill="black"/>
|
||||
<path d="M112.678 9.89597H113.524V10.6111C113.775 10.3163 114.041 10.1045 114.321 9.97595C114.602 9.84736 114.914 9.78306 115.257 9.78306C116.01 9.78306 116.518 10.0324 116.782 10.5311C116.927 10.804 117 11.1945 117 11.7026V14.9347H116.094V11.759C116.094 11.4516 116.046 11.2039 115.95 11.0157C115.792 10.702 115.505 10.5452 115.089 10.5452C114.878 10.5452 114.704 10.5656 114.569 10.6064C114.325 10.6754 114.11 10.8134 113.925 11.0204C113.777 11.1866 113.679 11.3591 113.633 11.5379C113.59 11.7135 113.569 11.966 113.569 12.2953V14.9347H112.678V9.89597Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/blog/a-better-way-for-consuming-content/banner.webp
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
src/assets/blog/how-computers-works/banner.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/blog/i-participated-in-a-hackathon/banner.png
Normal file
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/blog/peddler-app/banner.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/blog/rewind-2023-and-future-plans/2023-complete!.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/blog/rewind-2023-and-future-plans/banner.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/blog/the-monotony-of-social-media/banner.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/blog/website-2.0/banner.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src/assets/portfolio/build-a-fullstack-app/banner.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
src/assets/portfolio/human-to-js/banner.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/portfolio/next-intl-blog-template/banner.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
src/assets/portfolio/workarise/banner.png
Normal file
After Width: | Height: | Size: 74 KiB |
58
src/components/footer.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Code, RssIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import formatDate from "@/utils/format-date";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-secondary px-6 py-10 text-center text-sm font-light md:px-16">
|
||||
<section className="space-y-2">
|
||||
<p>
|
||||
Developed by{" "}
|
||||
<strong className="font-bold text-primary">juancmandev</strong>
|
||||
</p>
|
||||
<p>
|
||||
Built handcrafted with{" "}
|
||||
<Button
|
||||
asChild
|
||||
size={null}
|
||||
variant="link"
|
||||
className="m-0 p-0 text-base no-underline hover:underline"
|
||||
>
|
||||
<a href="https://astro.build/" target="_blank">
|
||||
Astro
|
||||
</a>
|
||||
</Button>
|
||||
</p>
|
||||
<p>Last built {formatDate(new Date())}</p>
|
||||
</section>
|
||||
<ul className="mx-auto mt-4 flex max-w-[200px] justify-between">
|
||||
<li>
|
||||
<Button
|
||||
asChild
|
||||
size={null}
|
||||
variant="link"
|
||||
className="flex flex-col justify-center"
|
||||
>
|
||||
<a target="_blank" href="https://github.com/juancmandev/website">
|
||||
<Code className="w-6" />
|
||||
Source Code
|
||||
</a>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
asChild
|
||||
size={null}
|
||||
variant="link"
|
||||
className="flex flex-col justify-center"
|
||||
>
|
||||
<a target="_blank" href="https://juancman.dev/rss.xml">
|
||||
<RssIcon className="w-6" />
|
||||
RSS feed
|
||||
</a>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
);
|
||||
}
|
30
src/components/link-button.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
variant?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link"
|
||||
| null
|
||||
| undefined;
|
||||
className?: string;
|
||||
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
||||
};
|
||||
|
||||
export default function LinkButton(props: Props) {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
size={props.size}
|
||||
variant={props.variant}
|
||||
className={props.className}
|
||||
>
|
||||
<a href={props.href}>{props.children}</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
13
src/components/mdx/astro-image.astro
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
const { src, alt } = Astro.props;
|
||||
---
|
||||
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={1092}
|
||||
height={986}
|
||||
class="w-auto h-auto rounded-md aspect-auto"
|
||||
/>
|
42
src/components/mdx/copy-button.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
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>
|
||||
);
|
||||
}
|
11
src/components/mdx/custom-anchor.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
type TAnchor = {
|
||||
href: string;
|
||||
} & React.HTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
export default function CustomAnchor(props: TAnchor) {
|
||||
return props.href.startsWith("/") || props.href.startsWith("#") ? (
|
||||
<a {...props} className="inline-flex outline-ring" />
|
||||
) : (
|
||||
<a {...props} className="inline-flex outline-ring" target="_blank" />
|
||||
);
|
||||
}
|
9
src/components/mdx/wrapper.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import AstroImage from "@/components/mdx/astro-image.astro";
|
||||
import CustomAnchor from "@/components/mdx/custom-anchor";
|
||||
|
||||
const components = {
|
||||
img: AstroImage,
|
||||
a: CustomAnchor,
|
||||
};
|
||||
|
||||
export default components;
|
41
src/components/microblog-item.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { MicroblogsResponse, TagsResponse } from "@/utils/pocketbase";
|
||||
import formatDate from "@/utils/format-date";
|
||||
|
||||
type Props = MicroblogsResponse<unknown> & {
|
||||
expand: {
|
||||
tags: TagsResponse[];
|
||||
};
|
||||
};
|
||||
|
||||
export default function MicroblogItem(props: Props) {
|
||||
return (
|
||||
<article className="rounded-sm border px-4 py-2">
|
||||
<header className="mb-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-light">
|
||||
{formatDate(new Date(props.published))}{" "}
|
||||
</span>
|
||||
<span className="text-sm font-thin">
|
||||
{new Date(props.published).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{props.expand &&
|
||||
props.expand.tags &&
|
||||
props.expand.tags.map(
|
||||
(tag) =>
|
||||
tag && (
|
||||
<span className="text-sm" key={tag.id}>
|
||||
#{tag.name}{" "}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<ReactMarkdown>{props.content}</ReactMarkdown>
|
||||
</main>
|
||||
</article>
|
||||
);
|
||||
}
|
54
src/components/mobile-menu.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
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>
|
||||
);
|
||||
}
|
52
src/components/navbar.astro
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
import { navItems } from "@/utils/nav-links";
|
||||
import MobileMenu from "@/components/mobile-menu";
|
||||
import logo from "@/assets/logo.png";
|
||||
import { Image } from "astro:assets";
|
||||
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}
|
||||
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) => (
|
||||
<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>
|
29
src/components/post-item.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import formatDate from "@/utils/format-date";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Props = {
|
||||
slug: string;
|
||||
date: Date | string;
|
||||
title: string;
|
||||
type: "blog" | "portfolio";
|
||||
};
|
||||
|
||||
export default function PostItem(props: Props) {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
size={null}
|
||||
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"
|
||||
>
|
||||
<a className="no-underline" href={`/${props.type}/${props.slug}`}>
|
||||
<span className="text-sm font-light no-underline">
|
||||
{formatDate(props.date)}
|
||||
</span>
|
||||
<span className="text-primary text-underline text-lg font-semibold underline">
|
||||
{props.title}
|
||||
</span>
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
55
src/components/ui/button.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-foreground hover:bg-secondary/60",
|
||||
ghost: "shadow-none hover:bg-foreground/10",
|
||||
link: "shadow-none underline-offset-4 hover:underline focus-within:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
46
src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
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 }
|
138
src/components/ui/sheet.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
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,
|
||||
}
|
122
src/content/blog/a-better-way-for-consuming-content.mdx
Normal file
@ -0,0 +1,122 @@
|
||||
---
|
||||
title: A Better Way for Consuming Content
|
||||
description: Get your news without visiting websites with algorithms that shows content that you don't want to see.
|
||||
tags: [Tech]
|
||||
image: /blog/a-better-way-for-consuming-content/banner.webp
|
||||
imageCaption: Newspapers. Photo by Ashni on Unsplash
|
||||
date: 2024-4-11
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _Photo by
|
||||
[Ashni](https://unsplash.com/@ashni_ahlawat?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/photos/text-ePWaAwUn80k?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||
|
||||
Get your news without visiting websites with algorithms that shows content that you don't want to see.
|
||||
|
||||
## Algorithms that Dictates What You See
|
||||
|
||||
Social media are not designed for showing you the latest and most important
|
||||
news, but for showing you content dictated by an algorithm.
|
||||
|
||||
And this content is normally viral, and viral doesn't mean interesting.
|
||||
|
||||
Usually this algorithm prioritizes content that get you angry.
|
||||
|
||||
Content that promotes negativity gets more clicks rather than those whose
|
||||
promotes positivity.
|
||||
|
||||
That's the reason why Twitter and Facebook are full of stupid and irrelevant
|
||||
posts (usually).
|
||||
|
||||
Of course, it's really cool when the algorithm shows you content that you like,
|
||||
discovering new people and pages, but that's not usual.
|
||||
|
||||
Without mentioning the annoying ads and more stuff that wants you to click it.
|
||||
|
||||
Meta (previously Facebook) knows about this, and encourages it in their products
|
||||
like Instagram and Facebook, and the same for Twitter.
|
||||
|
||||
## The Solution: News Aggregators (RSS)
|
||||
|
||||
RSS is an acronym for "Really Simple Syndication".
|
||||
|
||||
It's an ancient technology, not promoted so much by companies.
|
||||
|
||||
That's because when you read a post in an RSS Reader, you don't need to visit
|
||||
the website, and the website can't show advertisements using Google Ads (for
|
||||
example). You don't generate traffic; your visits don't count, at least not if
|
||||
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,
|
||||
not by a creepy algorithm that wants you to be mad.
|
||||
|
||||
[For my website](https://github.com/juancmandev/website/blob/main/scripts/rss.ts),
|
||||
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
|
||||
the metadata.
|
||||
|
||||
## How to Use an RSS Reader
|
||||
|
||||
First, you should download one.
|
||||
|
||||
There are many options:
|
||||
|
||||
- [NetNewsWire](https://netnewswire.com/): a native RSS Reader for macOS and
|
||||
iOS, free and Open Source, my favorite option as an Apple ~~Sinner~~ user
|
||||
- [Akregator](https://apps.kde.org/akregator): from the KDE project for Linux
|
||||
- [Feeder](https://play.google.com/store/apps/details?id=com.nononsenseapps.feeder.play):
|
||||
for Android
|
||||
- [Raven Reader](https://ravenreader.app/): desktop cross-platform
|
||||
|
||||
### Adding Feeds
|
||||
|
||||
Now you need to search for the RSS URL on your favorite website, like
|
||||
[this one](https://juancman.dev/rss.xml)!
|
||||
|
||||
```
|
||||
https://juancman.dev/rss.xml
|
||||
```
|
||||
|
||||
If you open it, you'll get a weird page with code similar to HTML.
|
||||
|
||||
Once copied, go to your RSS feed and search for "Add feed" or something similar,
|
||||
and paste the link, and you're done! Now you'll get the latest posts from my
|
||||
website.
|
||||
|
||||
### Adding Social Media Feeds
|
||||
|
||||
You can even add feeds from sites like Reddit or YouTube.
|
||||
|
||||
#### Reddit
|
||||
|
||||
Just change `[SUBREDDIT]` for the name of your subreddit to add:
|
||||
|
||||
```
|
||||
https://reddit.com/r/[SUBREDDIT]/new/.rss
|
||||
```
|
||||
|
||||
#### YouTube
|
||||
|
||||
Go to the channel to add, then go to the **About** tab, then click on **Share >
|
||||
Copy channel ID**.
|
||||
|
||||
Now just change `[CHANNEL ID]` for the copied one:
|
||||
|
||||
```
|
||||
https://youtube.com/feeds/videos.xml?channel_id=[CHANNEL ID]
|
||||
```
|
||||
|
||||
### My Favorite Feeds
|
||||
|
||||
- [juancman.dev (obviously!)](https://www.juancman.dev/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 (cool guy)](https://ericmurphy.xyz/index.xml)
|
||||
|
||||
## More About 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/)
|
156
src/content/blog/how-computers-works.mdx
Normal file
@ -0,0 +1,156 @@
|
||||
---
|
||||
title: How Computers Works
|
||||
description: Today we use, in some way, the computer in almost every activity in our lives, it could be for work or just fun, but if we think carefully, computers are an invention from the previous century, and have changed our lives.
|
||||
tags: [Tech, Informatic]
|
||||
image: /blog/how-computers-works/banner.jpg
|
||||
imageCaption: An open laptop. Photo by Philipp Katzenberger on Unsplash
|
||||
date: 2023-5-29
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _An open laptop. Photo by
|
||||
[Philipp Katzenberger](https://unsplash.com/@fantasyflip?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText')
|
||||
on
|
||||
[Unsplash](https://unsplash.com/photos/iIJrUoeRoCQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)_
|
||||
|
||||
Today we use, in some way, the computer in almost every activity in our lives, it could be for work or just fun, but if we think carefully, computers are an invention from the previous century, and have changed our lives.
|
||||
|
||||
New works have appeared, new careers to study, and new problems to be solved.
|
||||
|
||||
However, do we know how computers penetrated our lives? Do we know how a
|
||||
computer works? How does the Internet work?
|
||||
|
||||
Many people use their smartphones to communicate with family and friends and to
|
||||
share their lives, but they don’t know how all this is possible.
|
||||
|
||||
I’m not saying that everyone needs to be a Software Engineer or IT Expert, but
|
||||
knowing about this could be outstanding knowledge.
|
||||
|
||||
## The power of computers
|
||||
|
||||
Computers can expand our brains, such things like sends messages to people from
|
||||
the other side of the Earth, to create an app that speeds up delivery.
|
||||
|
||||
All these things are possible by flipping 0’s and 1’s, but how is this possible?
|
||||
|
||||
If you've watched The Imitation Code, maybe you know this story.
|
||||
|
||||
### Computing Fundaments
|
||||
|
||||
Alan Turing was the inventor of the Turing Machine, a simple but powerful
|
||||
machine that can receive instructions to move along a long tape, changing the
|
||||
state of each slot. This three things, a head, a long tape and a set of
|
||||
instructions are the bases for the modern computers.
|
||||
|
||||
The head is the Central Processing Unit (CPU), a piece of hardware that can be
|
||||
used for general purposes, receiving instructions (Algorithm) whose are
|
||||
transformed to electric pulses, understanding if electricity pass trough or not,
|
||||
if it's true or false, 1 or 0. All these instructions are saved in a Random
|
||||
Access Memory (RAM) for a quick access of the work that needs to be
|
||||
accomplished, and using a Read-Only Memory (ROM) to store persistent data that
|
||||
needs to be saved even if the computer shuts down.
|
||||
|
||||
An algorithm's like a recipe, declaring ingredients (variables) and the steps to
|
||||
follow to achieve the result (functions).
|
||||
|
||||
A variable is an identifier that points to a slot of memory in the RAM, storing
|
||||
a value that can be a number, a text (known as “string”), a boolean (true or
|
||||
false), or an object (a set of multiple variables and functions that can be
|
||||
instanced), etc.
|
||||
|
||||
Functions are blocks of instructions that achieve a task, like obtaining your
|
||||
current location or sending a message.
|
||||
|
||||
And maybe you are asking, how do I tell a computer how to do what I want?
|
||||
|
||||
### Programming languages
|
||||
|
||||
If you try to speak with someone who doesn’t speak the same language as you, you
|
||||
try to use a translator or use gestures, something that you know that both can
|
||||
in some way understand, the same is for computers.
|
||||
|
||||
Computers are powerful, but they need someone to tell them what to do, this is
|
||||
work for humans, and to achieve it we use programming languages. With a
|
||||
programming language you use a specific syntax to tell a computer your desired
|
||||
task, then you compile that file where you type all your instructions, when a
|
||||
file compiles, is transformed to a computer nature language (1’s and 0’s) and
|
||||
then the computer executes the task.
|
||||
|
||||
There are different programming languages, and all of them are designed to
|
||||
achieve specific needs, like the programming languages C and C++, both are
|
||||
low-level languages, which means that are close to how a computer “speaks” and
|
||||
are used to control and administrate memory in high-efficient apps, or to
|
||||
illuminate the screen of your computer.
|
||||
|
||||
There is Java, is a language that can create an environment when is compiled,
|
||||
meaning that can be used on almost every computer.
|
||||
|
||||
JavaScript (is NOT Java or something like that) is a language that our browsers
|
||||
understands, with the help of JavaScript, we can access to a web page and see
|
||||
nice interactions when we click a button, login with a username and password,
|
||||
and more.
|
||||
|
||||
JavaScript is a high-level language, that is easiest to learn than Java or C,
|
||||
but not that is worse or better, just resolves a different need.
|
||||
|
||||
### The Browser
|
||||
|
||||
A powerful software that can access other computers using the Hyper Text
|
||||
Transfer Protocol (HTTP), it means that thanks to this protocol different
|
||||
computers can send and receive information to communicate, even if they’re far
|
||||
away. The browsers receive data in the form of a file, mainly three:
|
||||
|
||||
- Hyper Text Markup Language (HTML)
|
||||
- Cascading Styles Sheet (CSS)
|
||||
- JavaScript
|
||||
|
||||
### HTML
|
||||
|
||||
Helps the browser to structure data like texts or images, using a markup
|
||||
language (tags). The browser can know where to put an input to type your email
|
||||
or a button to subscribe to your favorite artist.
|
||||
|
||||
### CSS
|
||||
|
||||
It gives colors and forms to the HTML tags using selectors. It can be used to
|
||||
change page's background color, change button's rounded borders, modify text
|
||||
color, and everything your creativity can give.
|
||||
|
||||
### JavaScript
|
||||
|
||||
Combining HTML and CSS with JavaScript creates an interactive web site, or web
|
||||
app (like this one). You can for example, add a button that changes the theme to
|
||||
dark/light, or store items in a shopping cart and show a number of items you
|
||||
have.
|
||||
|
||||
For all this you need to store your files somewhere, letting people access a
|
||||
computer to download all these files using their browsers, that's the
|
||||
functionality of servers.
|
||||
|
||||
Servers are computers that are connected to the Internet, and store files that
|
||||
can be downloaded or uploaded using protocols and security rules. Some companies
|
||||
like Google or Microsoft have multiple centers with many servers in different
|
||||
regions of the planet, called Data Centers, and can be used with a fee for
|
||||
storing your web app, these multiple Data Centers are called Cloud.
|
||||
|
||||
### The Cloud
|
||||
|
||||
Administrating a powerful computer can be difficult, but if you know how to use
|
||||
it, you can save a lot of money instead of maintaining local computers that need
|
||||
to be turned on 24/7. Thanks to the cloud we can deliver the fastest apps, and
|
||||
we can have a 24/7 service for our customers with a marginal cost.
|
||||
|
||||
## Computers Changed Humanity
|
||||
|
||||
Computers simplifies our daily tasks, software can be easy replicated and
|
||||
distributed without need of logistics like a tangible product. You just need an
|
||||
Internet connection to reach someone’s project.
|
||||
|
||||
You don’t need a factory or natural resources like wood to produce paper, you
|
||||
need a group of engineers, UX/UI designers, digital marketers, and more IT
|
||||
people to reach billions of customers.
|
||||
|
||||
The reason that computers are too powerful is that the marginal cost is minimal,
|
||||
you don’t need to extract something from the earth to build an app, you need a
|
||||
group of talented persons that uses their brains to create solutions.
|
64
src/content/blog/i-participated-in-a-hackathon.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: I participated in a Hackathon
|
||||
description: I recently participated in a Supabase Hackathon, forming a team with people from other countries.
|
||||
tags: [Tech, Hobby, Hackaton]
|
||||
image: /blog/i-participated-in-a-hackathon/banner.png
|
||||
imageCaption: 'Tech stack used: Supabase, Next.js and Shadcn/UI, My first hackathon!'
|
||||
date: 2023-8-16
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _Tech stack used:
|
||||
Supabase, Next.js and Shadcn/UI, My first hackathon!_
|
||||
|
||||
I recently participated in a Supabase Hackathon, forming a team with people from other countries.
|
||||
|
||||
The Hackathon thematic was free, the only main rule is to use Supabase for any
|
||||
feature, like authentication, as a PostgreSQL database or using vectors for AI,
|
||||
with 10 days to build a product using any technology and upload it in a GitHub
|
||||
repository.
|
||||
|
||||
We developed an e-commerce app with a Walmart products model, implementing
|
||||
vectors for better search results.
|
||||
|
||||
Using Supabase we implemented auth and protected routes so the user needs to log
|
||||
in to see recommendations and more.
|
||||
|
||||
The user can add products to the shopping cart and check their items for saved
|
||||
it and see recommendations and which items are frequently bought.
|
||||
|
||||
My main role was focused on creating the UI using the Next.js 13 app router,
|
||||
protecting routes only for authenticated users, and create reusable components
|
||||
such as product cards, and of course, making the layout responsive for mobile
|
||||
and desktop.
|
||||
|
||||
We used [Shadcn/UI](https://ui.shadcn.com/) as those components are already
|
||||
implemented functionalities with accessibility like modals or sidebars, like the
|
||||
sidebar that appears when you’re on a mobile device and open the button in the
|
||||
header, with a smooth animation.
|
||||
|
||||
We submitted the project on time and waiting for the results, and this is my
|
||||
first time participating in a Hackathon, I really enjoy it and hope to continue
|
||||
contributing to the project on GitHub.
|
||||
|
||||
It's amazing work with people from other countries, using English even if is not
|
||||
our native language, but with a purpose in common, create a great product.
|
||||
|
||||
I’ll keep looking to participate in more Hackathons in the future and contribute
|
||||
to open source projects on Github because I really enjoy the feeling of
|
||||
developing something big with more people.
|
||||
|
||||
I learned too much in these few days, like integrating Next.js with Supabase for
|
||||
authentication and protected routes, using the Supabase docs as a guide, and
|
||||
using it for the first time Shadcn/UI, and looking forward to keep using it.
|
||||
|
||||
It took me so long to participate in a Hackathon, as before I hesitated about my
|
||||
experience, but the reality is we’ll never be ready for new challenges because
|
||||
if you’re already ready it means that’s too late.
|
||||
|
||||
I want to learn more about using vectors for AI, so I’ll investigate more about
|
||||
the topic, as the tech tends to go that way, who knows what would be the next
|
||||
big tech trend or when.
|
||||
|
||||
[You can see the project: Grocewise here](https://groce-wise.vercel.app/)
|
@ -0,0 +1,29 @@
|
||||
---
|
||||
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.
|
||||
tags: [Personal]
|
||||
image: /blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg
|
||||
imageCaption: Letters mixed
|
||||
date: 2024-2-20
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||

|
||||
_Mixed letters. Photo by
|
||||
[Jason Leung](https://unsplash.com/@ninjason?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||
on
|
||||
[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.
|
||||
|
||||
That's why I have decided to only create content in English for now and in the
|
||||
future.
|
||||
|
||||
It doesn't mean that I reject my mother tongue or something like that, but just
|
||||
practicality.
|
||||
|
||||
I'll try to update the links to my posts shared on social media, as this website
|
||||
will be just juancman.dev/blog instead of juancman.dev/[locale]/blog.
|
||||
|
||||
However, if you want to contact me in Spanish, feel free to do it.
|
65
src/content/blog/peddler-app.mdx
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
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.
|
||||
tags: [Tech, SideProject]
|
||||
image: '/blog/peddler-app/banner.png'
|
||||
imageCaption: Peddler App provisional logo
|
||||
date: 2023-12-11
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _Peddler App
|
||||
provisional logo_
|
||||
|
||||
## The idea
|
||||
|
||||
You hear the ice cream man in his truck, you try to catch him, but the guy is already far away.
|
||||
|
||||
Why not get a notification on your phone when the ice cream man is near you? So
|
||||
you can just tap the notification and request the ice cream man to go to your
|
||||
location.
|
||||
|
||||
That's the purpose of this app.
|
||||
|
||||
## Overwhelming for me
|
||||
|
||||
I'd never developed a big app just by myself, but I want to try and see what
|
||||
happens.
|
||||
|
||||
I want to follow the Indie Hacker way, sharing in public the progress, and
|
||||
getting feedback from the community.
|
||||
|
||||
This post is the first step before designing in Figma or even creating the
|
||||
landing page, so I want to hear if you're interested, why you're not, or what
|
||||
would be great for the app.
|
||||
|
||||
## Starting small
|
||||
|
||||
I really want to start small, launching a Minimum Viable Product (MVP), with the
|
||||
next features:
|
||||
|
||||
- User registration and login
|
||||
- Two types of users, peddlers and customers
|
||||
- Peddlers
|
||||
- If the user is a peddler (wants to sell), redirect to the peddler form
|
||||
- Peddlers can create a profile with the name of their company and products
|
||||
that offer, for example ice cream, candy, etc
|
||||
- Once the registration is finished, peddlers can start routes
|
||||
- The app gets the location of the peddler, showing it on a map
|
||||
- The backend will detect if the peddler enters a radius of a customer, and
|
||||
send a push notification to the customer
|
||||
- Once the peddler has a request, the app will show the location of the
|
||||
requesting customer on the map
|
||||
- The peddler can go to the destination, and fulfill the transaction
|
||||
- Customers
|
||||
- Customers can just create a profile with their name or alias, and set
|
||||
locations, for example: house
|
||||
- Once a peddler is near, the backend will send a notification to the customer
|
||||
- If the customer taps and confirms the notification
|
||||
|
||||
Of course, customers would change notifications settings and more, but that's
|
||||
the core idea.
|
||||
|
||||
For the MVP I don't want to implement in-app payments, but of course, it could
|
||||
be a future feature.
|
109
src/content/blog/rewind-2023-and-future-plans.mdx
Normal file
@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Rewind 2023 and future plans
|
||||
description: My rewind for 2023 and my plans for 2024 and beyond.
|
||||
tags: [Thoughts]
|
||||
image: /blog/rewind-2023-and-future-plans/banner.jpg
|
||||
imageCaption: A sunset with a sign, photo by Javier Allegue Barros on Unsplash
|
||||
date: 2023-12-16
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _Photo by
|
||||
[Javier Allegue Barros](https://unsplash.com/@soymeraki?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/photos/silhouette-of-road-signage-during-golden-hour-C7B-ExXpOIE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||
|
||||
My rewind for 2023 and my plans for 2024 and beyond.
|
||||
|
||||
I hope you are having a good time this holiday.
|
||||
|
||||
Life is a succession of choices, and in retrospect, I'm happy that this year I
|
||||
made the right ones.
|
||||
|
||||
## In retrospect about my career this 2023
|
||||
|
||||
I grew up drastically as a Full Stack Developer, learning new libraries and
|
||||
establishing my tech stack.
|
||||
|
||||
I even started hacking (not the thing you hear in the news), creating side
|
||||
projects looking to create solutions.
|
||||
|
||||
I updated this website, creating new functionalities for content creation.
|
||||
|
||||
Now I have more confidence in my skills, ready to keep growing up and taking on
|
||||
new challenges.
|
||||
|
||||
## Futures plans for my career this 2024
|
||||
|
||||
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
|
||||
backend and cloud, as well as keep practicing my English to enter the USA or
|
||||
European markets.
|
||||
|
||||
Maybe I'll not achieve this in 2024, but I must keep growing, as each year
|
||||
passes, I'll be more prepared.
|
||||
|
||||
I'll check if I could contribute to an Open Source project, as almost every tool
|
||||
that I use is an Open Source one, I couldn't be here without if not with the
|
||||
help of Open Source projects.
|
||||
|
||||
Honestly, my true wish is to work full time in a Software as a Service startup,
|
||||
or any startup with a focus on Software.
|
||||
|
||||
The good thing that is I keep growing professionally, and I expect (and will)
|
||||
the next year I'll reach more milestones.
|
||||
|
||||
And of course, I'll continue hacking (in a creative way, not stealing info or
|
||||
criminal things) with side projects, creating an extra incoming source will be
|
||||
great for my finances.
|
||||
|
||||
## Retrospect of 2023 personally and more
|
||||
|
||||
This year I rediscovered the hobby of reading, and I really enjoy it.
|
||||
|
||||
I discovered my new favorite book, **Ready Player One**, it was a really
|
||||
exciting and great lecture, the next year I'll read the sequel.
|
||||
|
||||
I also started reading **Ikigai**, to keep acquiring good habits for a long and
|
||||
happy life, and seek a purpose in life.
|
||||
|
||||
Another book I started reading is **The Little Book of Common Sense Investing**,
|
||||
as I already had a good habit of saving, but I want my money to keep growing
|
||||
more for a dignified retirement.
|
||||
|
||||
I'm spending less time on social media,
|
||||
[even I wrote an article about this](https://www.juancman.dev/en/blog/the-monotony-of-social-media),
|
||||
doing things that **I really want to do** instead.
|
||||
|
||||
Is really horrible how much time social media steals from us, keeping us away
|
||||
from doing things that we really enjoy.
|
||||
|
||||
I'm doing moderate exercise, but indoors, I want to go outdoors too, I need more
|
||||
solar light.
|
||||
|
||||
I'm happy spending my time with my family, even if the majority of the time I'm
|
||||
working or studying, I keep contact with my loved ones, and continue doing it,
|
||||
clearly.
|
||||
|
||||
## Future plans for 2024 personally and more
|
||||
|
||||
I'll keep reading, I'll write more on this website.
|
||||
|
||||
I want to acquire new habits, like pixel art.
|
||||
|
||||
And I'm retaking an old hobby, **GameDev**.
|
||||
|
||||
As Unity is doing questionable things, I'll be using
|
||||
[Godot](https://godotengine.org/) instead.
|
||||
|
||||
I don't have in mind a big project or something like that, just retake to
|
||||
develop simple videogames demos, it would be great to launch a little videogame,
|
||||
but a complete one.
|
||||
|
||||
Trust me, is really, **REALLY** challenging to develop videogames, so instead of
|
||||
overwhelming myself, I'll keep my **ambitions simple**, but **constant**.
|
||||
|
||||
### Happy holidays!
|
||||
|
||||

|
92
src/content/blog/the-monotony-of-social-media.mdx
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
title: The Monotony of Social Media
|
||||
description: Abstracting human interaction using software has caused many problems that didn’t exist before.
|
||||
tags: [Thoughts]
|
||||
image: /blog/the-monotony-of-social-media/banner.jpg
|
||||
imageCaption: Person checking social media. Photo by Austin Distel on Unsplash
|
||||
date: 2023-7-17
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||

|
||||
_Person checking social media. Photo by
|
||||
[Austin Distel](https://unsplash.com/@austindistel?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)
|
||||
on
|
||||
[Unsplash](https://unsplash.com/photos/person-using-both-laptop-and-smartphone-tLZhFRLj6nY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
|
||||
|
||||
Abstracting human interaction using software has caused many problems that didn’t exist before.
|
||||
|
||||
It’s obvious, a lot of interactions on the Internet occurs through social media,
|
||||
letting you send friend requests, chat, or share memes and photos. However,
|
||||
abstracting human interaction using software has caused many problems that
|
||||
didn’t exist before.
|
||||
|
||||
When the Internet started, many people created their own websites because that's
|
||||
was the thing you must have if you want to be cool, and a lot of these websites
|
||||
was just blogpost-like where the users shares their hobbies like movies, sports,
|
||||
books, videogames, etc. This motivation made more unique and human websites,
|
||||
where you can meet someone and their likes or dislikes.
|
||||
|
||||
Now, with the boom of social media in the middle of 2000's, people prefer to
|
||||
connect just searching for a name or looking the friends of their friends, and
|
||||
send friend requests to try and connect. That was cool at the start, as anything
|
||||
new, but the problems started when companies like Facebook (now Meta) or Google
|
||||
(with YouTube) needed to monetize their platforms, mostly with Ads.
|
||||
|
||||
And of course, that means that they needed to suppress, censor or ban anything
|
||||
that could be harmful for society, like hate speeches or stupid challenges that
|
||||
could risk people's lives.
|
||||
|
||||
But, the bat thing about this, is that they homogenize almost everyone, forcing
|
||||
them to act as the algorithms keeps recommending users with likes and comments,
|
||||
guiding the people to act like someone else, and so.
|
||||
|
||||
Now almost everyone does mainly two things, post photos about their "perfect"
|
||||
lives or share memes, and don't get me wrong, it's ok to enter to social media
|
||||
and try to disconnect for your job or problems, but using this every day as
|
||||
instant escape instead of confronting your own problems could be harmful in the
|
||||
long term, isolating you from the need of socialize in real life, with real
|
||||
people, and thinking that everyone has a perfect life.
|
||||
|
||||
No, EVERYONE has problems in their lives, even more than yours, but social media
|
||||
algorithms promotes mainly "positive vibes only" and all that shit that in big
|
||||
dose is hamrful for our minds.
|
||||
|
||||
And don't mention the censorship and shadow-banning if you post something
|
||||
controversial, it could be something that should not be tolerated like incite
|
||||
hate to a group, or it could be something that don't everyone agrees but it
|
||||
could be useful think a little about it, but still being controversial.
|
||||
|
||||
Should everyone can tell what they thinks? Yeah, always if doesn't promotes hate
|
||||
or hurt other people or animals.
|
||||
|
||||
They're reports like Twitter promotes hate in the algorithm, and Meta knows that
|
||||
Instagram increases anxiety and depression on young people, and actually;
|
||||
promotes it... as all negative feelings keeps you on the social media
|
||||
interacting with others, as that's what those companies sell, your data and time
|
||||
to advertisers.
|
||||
|
||||
I recently heard a video talking about this topic, and that would be cool if we
|
||||
go back as the starts of the Internet where people created content as a hobby,
|
||||
instead of looking for validation through likes and comments, being more
|
||||
authentic persons instead of products.
|
||||
|
||||
Should software solutions replace human interactions? I think not, but it's too
|
||||
late for almost everyone, but if you're reading this, start changing your life
|
||||
first if you want to be honest with yourself.
|
||||
|
||||
Blog inspired by "Why does every personal website look like this now?" by Eric
|
||||
Murphy on YouTube.
|
||||
|
||||
Source Video:
|
||||
|
||||
<iframe
|
||||
width='100%'
|
||||
height='320'
|
||||
className='rounded-md'
|
||||
src='https://www.youtube-nocookie.com/embed/_x6SCSz7g5I'
|
||||
title='YouTube video player'
|
||||
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
></iframe>
|
115
src/content/blog/website-2.0.mdx
Normal file
@ -0,0 +1,115 @@
|
||||
---
|
||||
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.
|
||||
tags: [Tech]
|
||||
image: /blog/website-2.0/banner.png
|
||||
imageCaption: Tech Stack of this website. Next.js, Vercel, React.js, TypeScript and TailwindCSS
|
||||
date: 2023-4-7
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
 _Tech Stack of this website.
|
||||
Next.js, Vercel, React.js, TypeScript and TailwindCSS_
|
||||
|
||||
I commited some errors when creating the first version of my website, here I will share what I have learned.
|
||||
|
||||
The first version of my website was one of my biggest projects so fat, but
|
||||
**now** as I’ve **more** **experience** as a **Frontend** Engineer, I realize I
|
||||
**didn’t** do **enough** **research** into **creating** a **web** site with a
|
||||
**blog**.
|
||||
|
||||
## Client Side Rendering (CSR) vs Server Side Rendering (SSR) vs Static Generation (SG)
|
||||
|
||||
When developing a **web** **application**, you should **think** **about** the
|
||||
type of **rendering** to use, while **considering** the **requirements** of the
|
||||
**problems** you want to **solve**.
|
||||
|
||||
### Client Side Rendering (CSR)
|
||||
|
||||
For example, a web **application** like a **SaaS** to create tasks and manage
|
||||
people will have **dynamic** **pages** to show the tasks, update the cards when
|
||||
is edited or deleted, show notifications, etc. In this situation, a **CSR**
|
||||
would be **better**, to **render** the page **each** time the user **request**
|
||||
access will **keep** the data **updated**. However, a **CSR** needs to
|
||||
**hydrate** the page when is **requested**, this causes a **slow** **first**
|
||||
**load**, and uses **more** **resources** of the user’s PC.
|
||||
|
||||
### Server Side Rendering (SSR)
|
||||
|
||||
This could be solved using **SSR**, this consist in **generate** the page in the
|
||||
**Server** where the web app is hosted **using** all the **power** that a
|
||||
**server** can provide. The problem is that a **server** is **required**, Google
|
||||
Cloud provides with serverless options like App Engine or Cloud Run, but you’ll
|
||||
need to learn about this services and how to deploy the project, so the
|
||||
**technical** **knowledge** is **high**.
|
||||
|
||||
The **disadvantage** of **CSG** and **SSR** is that because **each** **page**
|
||||
**must** be **rendered** on each **request**, **web** **crawlers** and
|
||||
**search** **engines** such as Google's will take **longer** to **obtain**
|
||||
**information** about your page, resulting in a **low** **SEO** priority.
|
||||
|
||||
### Static Generation (SG)
|
||||
|
||||
Well, if a **page** **doesn’t** require **fetch** **data** for **each**
|
||||
**request**, then you could use **SG**, this means that the **page** is
|
||||
**generated** when you **build** the **production** directory **before** you
|
||||
**deploy** it. The page will be generated into a HTML/CSS/JS one time, and if
|
||||
you need to **update** the **data** in that page you’ll need to do the
|
||||
**changes** and **deploy** the project. Yes, you’ll need to be more cautious
|
||||
when reviewing the changes before deploying, but as the page is already
|
||||
generated, **web** **crawlers** and the Google’s **search** **engine** will get
|
||||
the **info** in your page **faster**, **improving** your **SEO**.
|
||||
|
||||
## The cool thing about Next.js
|
||||
|
||||
In the **past** you will require to **think** if go **full** CSR, SSR or SG,
|
||||
linking your web to their respecting sections, like the app, blog, etc.
|
||||
|
||||
**[Next.js](https://nextjs.org/)** is a **Node.js** **meta-framework** that uses
|
||||
**[React.js](https://react.dev/)** to build the UI, and provides with CSR, SSR,
|
||||
SG and more, so you can generate SG fetching async data, allowing you to don’t
|
||||
**need** to **create** **every** **static** **page**.
|
||||
|
||||
Is that the **approach** **used** for **this** **web** site, **instead** of
|
||||
**fetch** the data on **each** **request**, I only **fetch** data **when** I
|
||||
**create** the **build** of the project.
|
||||
|
||||
**Each** **article** is a SG page, but I use a **template** to keep every blog
|
||||
similar, using **markdown** syntax for the content of the blog, and with an
|
||||
**extension** of **TailwindCSS** I keep the styles consistent.
|
||||
|
||||
So, **Next.js** allows you to **choose** the **rendering** method for **each**
|
||||
**page** in your web, this feature permits to create amazing websites in the
|
||||
same project, keeping consistence and with fast load times, Next.js even lazy
|
||||
loads each page and start loading when you hover a link like Home, Contact, etc.
|
||||
|
||||
> I’ll explain in more detail the architecture of this project in the future!
|
||||
|
||||
## TailwindCSS vs MUI
|
||||
|
||||
I choose to use **[TailwindCSS](https://tailwindcss.com/)** to learn about this
|
||||
CSS library, and I’m impressed the **faster** that makes the development of the
|
||||
styles of a web project. **MUI** **provides** **functionalities**, but sometimes
|
||||
**gives** **problems** with **hydration** like in my previous website, when you
|
||||
**first** **load** the page it takes a **time** to **show** all the **styles**,
|
||||
now it no longer occurs because TailwindCSS is pure CSS and the pages are
|
||||
static.
|
||||
|
||||
## Deploying on Vercel
|
||||
|
||||
**[Vercel](https://vercel.com)** is the company behind Next.js, and they
|
||||
provides with **hosting** services **optimized** for **Node.js** apps, and as
|
||||
I’m learning about Cloud Development maybe I could try to host this web like my
|
||||
previous web into a Cloud Service like Cloud Run, but this time I choose to use
|
||||
**Vercel** to get the **analytics** that are very useful, and as the **hobby
|
||||
plan** gives me free hosting for small projects.
|
||||
|
||||
To deploy I use the **[Vercel CLI](https://vercel.com/docs/cli)**, pretty simple
|
||||
and straightforward.
|
||||
|
||||
## More content coming soon!
|
||||
|
||||
I’ll keep updating with posts, features and more content to share my experience,
|
||||
and know I’m writing this paragraph, I think the next feature it would be a
|
||||
newsletter to notify people when I create a new post. Time to work!
|
37
src/content/config.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const contentSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
image: z.string(),
|
||||
imageCaption: z.string(),
|
||||
date: z.coerce.date(),
|
||||
tags: z.array(z.string()),
|
||||
author: z.string(),
|
||||
rss: z.boolean(),
|
||||
draft: z.boolean({}).optional(),
|
||||
});
|
||||
|
||||
const blog = defineCollection({
|
||||
type: "content",
|
||||
schema: contentSchema,
|
||||
});
|
||||
|
||||
const portfolio = defineCollection({
|
||||
type: "content",
|
||||
schema: contentSchema,
|
||||
});
|
||||
|
||||
const pages = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
blog,
|
||||
portfolio,
|
||||
pages,
|
||||
};
|
22
src/content/pages/about.mdx
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# About
|
||||
|
||||
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.
|
||||
|
||||
I'm trying to expand my skills, as I'm a Frontend Developer (with knowledge on
|
||||
Backend), but skills like writing are important.
|
||||
|
||||
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
|
||||
only exception is [LanguageTool](https://languagetool.org/) for validating my
|
||||
grammar.
|
||||
|
||||
[](https://notbyai.fyi/)
|
36
src/content/pages/contact.mdx
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Contact
|
||||
description: You can contact me if you want me to work, or just say hello.
|
||||
---
|
||||
|
||||
# Contact
|
||||
|
||||
You can contact me if:
|
||||
|
||||
- You want me to work
|
||||
- Just say hello
|
||||
|
||||
Please consider that **I don't**:
|
||||
|
||||
- Work for free
|
||||
- Work on your startup idea and just get equity in return (I can't pay my bills
|
||||
with lottery tickets)
|
||||
- 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
|
||||
Discord, Slack, etc. once you hire me).
|
||||
|
||||
## My email
|
||||
|
||||
Just change `[at]` for `@` and `[dot]` for `.`. This is for preventing web
|
||||
crawlers from getting my email:
|
||||
|
||||
```
|
||||
contact[at]juancman[dot]dev
|
||||
```
|
||||
|
||||
## Social media
|
||||
|
||||
You can send me a direct message:
|
||||
|
||||
- [LinkedIn](https://www.linkedin.com/in/juancmandev)
|
||||
- [GitHub](https://github.com/juancmandev)
|
62
src/content/pages/resources.mdx
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Resources
|
||||
description: Here you can find websites, YouTube channels, courses and more stuff that I consume or find interesting.
|
||||
---
|
||||
|
||||
# Resources
|
||||
|
||||
Here you can find websites, YouTube channels, courses and more stuff that I
|
||||
consume or find interesting.
|
||||
|
||||
## Programming and Web Development
|
||||
|
||||
To **power-up** my career.
|
||||
|
||||
### Websites (courses, docs, etc.)
|
||||
|
||||
- [fireship.io](https://fireship.io) - My favorite premium courses about WebDev
|
||||
|
||||
- [MDN Web Docs](https://developer.mozilla.org/en-US) - Best docs for HTML, CSS
|
||||
and JS
|
||||
|
||||
### Tech Stack
|
||||
|
||||
Technologies that I use for personal projects and sometimes for work.
|
||||
|
||||
- [Next.js](https://nextjs.org) - Dynamic and flexible React meta-framework,
|
||||
used on this Website
|
||||
|
||||
- [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
|
||||
|
||||
---
|
||||
|
||||
## Inspiration and Learning
|
||||
|
||||
For writing, thinking or growing my career.
|
||||
|
||||
### Personal Websites
|
||||
|
||||
- [Eric Murphy](https://ericmurphy.xyz) - The guy who inspired me to retake this
|
||||
side project and create more content
|
||||
|
||||
- [Bikobatanari](https://www.bikobatanari.art) - Super original, fun and source
|
||||
of inspiration
|
||||
|
||||
### 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) -
|
||||
Today people prefers talk about celebrities and other peoples lifes rather
|
||||
than our own milestones
|
||||
|
||||
- [My Website is a Personal Museum](https://www.bikobatanari.art/posts/2020/personal-museum) -
|
||||
The digital form of the three house
|
1219
src/content/portfolio/build-a-fullstack-app.mdx
Normal file
76
src/content/portfolio/human-to-js.mdx
Normal file
@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Human to JS
|
||||
description: Translate human language to JavaScript code!
|
||||
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_
|
||||
|
||||
_This project has been achieved._
|
||||
|
||||
## Background
|
||||
|
||||
I’m always looking to grow my career by learning new technologies as well known
|
||||
Software Engineer; however, that could be dangerous because Software Engineer is
|
||||
not about using the ultimate tech stack but making things happen.
|
||||
|
||||
## Inspiration source
|
||||
|
||||
I was checking Twitter until I found a tweet where a person created a side
|
||||
project over a weekend. That project is
|
||||
[SQL Translator](https://www.sqltranslate.app/), a simple UI to put text input
|
||||
describing a query; then you get the query into SQL, simple!
|
||||
|
||||
[@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)
|
||||
used ChatGPT API to send a prompt typed by the user, and then show SQL response
|
||||
into a component to copy to the clipboard. That was enough to get the deserved
|
||||
attention of the community.
|
||||
[Tweet link](https://twitter.com/whoiskatrin/status/1634973237829599233)
|
||||
|
||||
## My idea
|
||||
|
||||
> _“Why not a web app to type a prompt to generate JavaScript code?”_
|
||||
|
||||
So I started to build my idea using this tech stack:
|
||||
|
||||
- **Next.js**: Web framework to build the UI and Next.js provides you with an
|
||||
API directory to communicate with ChatGPT API
|
||||
- **MUI**: To use the UI components and as a design system
|
||||
- **Formik & Yup**: To manage the state of the prompt form and create validation
|
||||
schemas
|
||||
|
||||
Using all these technologies I build a simple UI with a MUI Card component, then
|
||||
I created a form where I manage all the inputs with Formik, the text and select
|
||||
inputs are directly from MUI, and to create the validation schema I used Yup to
|
||||
mark as required those fields and don’t send them empty.
|
||||
|
||||
With the UI finished, I started creating the endpoint in the API directory to
|
||||
consume ChatGPT’s API, just using a fetch
|
||||
like [@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)’s
|
||||
project, indicating which OpenAI model to use, in this case, *text-davinci-003*,
|
||||
you can learn more about those
|
||||
models [here](https://platform.openai.com/docs/api-reference/models/list).
|
||||
Obviously, in that request, I send the prompt from the user into a string
|
||||
indicating ChatGPT to only give me the code, without comments or more results.
|
||||
|
||||
## Added value
|
||||
|
||||
Yes, there’re options like GitHub Copilot that resolve that problem, that’s why
|
||||
I added a select option to choose if the syntax should be an arrow function or a
|
||||
simple function.
|
||||
|
||||
I’ll add more features, like a TypeScript option, and use a TS Interface to use
|
||||
as a reference, but now I’m working on more projects!
|
||||
|
||||
## Inspiring people!
|
||||
|
||||
The cool thing about side projects is that inspires people like us, we can use
|
||||
our tech skill that serves the bread on the table to transform ideas into
|
||||
products, and products into a community, as
|
||||
[@Serudda](https://twitter.com/serudda) talks in this
|
||||
[video](https://www.youtube.com/watch?v=LXgPNdw8avI&t) (video audio in Spanish).
|
148
src/content/portfolio/next-intl-blog-template.mdx
Normal file
@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Next Intl Blog Template
|
||||
description: Start your blog in multiple languages!
|
||||
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)
|
||||
|
||||
## Overview
|
||||
|
||||
Recently I update this website, and as you may know, is an **English and Spanish
|
||||
content website**.
|
||||
|
||||
I'm not using a translation plugin, instead I write every work in both English
|
||||
and Spanish.
|
||||
|
||||
Thanks to Next.js and [next-intl](https://next-intl-docs.vercel.app/) I can
|
||||
achieve this, rendering routes for each language in the website, accessing a
|
||||
dictionary that contains the content translated by me.
|
||||
|
||||
For the .mdx files, I created a directory for each language, and inside of those
|
||||
directories it contains the content in both languages too.
|
||||
|
||||
## How to use
|
||||
|
||||
This template is an extension of
|
||||
[next-intl](https://next-intl-docs.vercel.app/), chek the
|
||||
[getting started](https://next-intl-docs.vercel.app/docs/getting-started) to
|
||||
learn the basics, the purpouse of the template is to create a simple layout for
|
||||
future customization.
|
||||
|
||||
### Add or remove locales
|
||||
|
||||
You can add or remove locales in the `src/lang/locales.ts` file.
|
||||
|
||||
```ts title="src/lang/locales.ts"
|
||||
export type locales = 'en' | 'es';
|
||||
|
||||
export const localesList: locales[] = ['en', 'es'];
|
||||
```
|
||||
|
||||
Just add or remove a locale from the `locales` const, and add or remove it from
|
||||
the list.
|
||||
|
||||
The first item in the `localesList` must be the default locale.
|
||||
|
||||
The list is used for static generation of the routes in
|
||||
`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 }));
|
||||
}
|
||||
```
|
||||
|
||||
Remember to update the matcher in `src/middleware.ts`.
|
||||
|
||||
```ts title="src/middleware.ts"
|
||||
//...
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/(en|es)/:path*'],
|
||||
};
|
||||
```
|
||||
|
||||
And of course, update your `src/lang/[locale].json` files.
|
||||
|
||||
### Content creation
|
||||
|
||||
Use `src/content/[locale]` for create content, in the `/[locale]/` directory
|
||||
ceate the directory for each purpouse, for example: `/[locale]/blog`.
|
||||
|
||||
Inside create the .mdx file with an unique name, the name will be used as the
|
||||
slug for create the static page for that post.
|
||||
|
||||
For create a blog section, you'll use the _getAllContent_ function in your
|
||||
route, for example: `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,
|
||||
}));
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
|
||||
This will create each static page for each blog post.
|
||||
|
||||
You can get the metadata of the .mdx file too.
|
||||
|
||||
```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,
|
||||
//...
|
||||
};
|
||||
}
|
||||
|
||||
//...
|
||||
```
|
||||
|
||||
Then, render the content using the _Mdx_ component.
|
||||
|
||||
```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} />;
|
||||
}
|
||||
```
|
||||
|
||||
[You can fork this template here](https://github.com/juancmandev/next-intl-blog-template)
|
74
src/content/portfolio/workarise.mdx
Normal file
@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Workarise
|
||||
description: Workarise Web App, manage tasks with your team.
|
||||
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)
|
||||
|
||||
## Overview
|
||||
|
||||
[Workarise](http://workarise.com) is a Team Manager Software as a Service to
|
||||
create task cards assigning people, set a start and due date, add attachments
|
||||
files, etc.
|
||||
|
||||
You can use the Calendar to see the tasks' due dates and schedule Google Meet
|
||||
events authorizing the use of your Google Calendar. You can edit and delete
|
||||
events which sync with your Google Calendar and guests' Google Calendars. The
|
||||
Gantt provides you with a timeline to check task duration.
|
||||
|
||||
The web app is developed with [React.js](https://react.dev/),
|
||||
using [Vite.js](https://vitejs.dev/) to run the development environment. For
|
||||
functionality like modals, and popovers we use [MUI](https://mui.com/). To
|
||||
manage the state of components we’re using useContext.
|
||||
|
||||
To create Google Meet events and sync the calendar we'd develop a small Node.js
|
||||
API to use Google OAuth 2 API, as we need to prompt our users to give access to
|
||||
their Google Calendars.
|
||||
|
||||
Currently, Workarise is in the first version,
|
||||
using [Firebase](https://firebase.google.com/) to authenticate users and store
|
||||
raw data and files. Firebase
|
||||
uses [Firestore](https://firebase.google.com/docs/firestore), a NoSQL DB,
|
||||
however we’re developing an API using [Django](https://www.djangoproject.com/)
|
||||
running on [Cloud Run](https://cloud.google.com/run) connected
|
||||
to [Cloud SQL](https://cloud.google.com/sql) to a MySQL instance, as we’ll be
|
||||
using a SQL DB in the future. Currently the web app and landing are deployed on
|
||||
Firebase Hosting, but we’ll move the landing page to Vercel, and it’ll be
|
||||
updated to use Next.js in the future to optimize SEO and publish blog posts.
|
||||
|
||||
## My impact in Workarise
|
||||
|
||||
Currently, we’re developing an MVP, and everyone is working part-time on this
|
||||
project. I joined in December, but before there wasn’t a product that users can
|
||||
use, so as I was the only Frontend Engineer at that moment I taked full
|
||||
responsibility for delivering something that can be considered an MVP.
|
||||
|
||||
It took me like 3 months to achieve that, I updated some dependencies of the
|
||||
project to improve the development flow, and I suggested using Firebase as
|
||||
Backend and Hosting.
|
||||
|
||||
Thanks to all this we got our first users and feedback, so we’re working on that
|
||||
feedback to keep improving our app, our users like the design and simplicity!
|
||||
|
||||
At the moment there’re 3 engineers in the team, 2 on the front (including me)
|
||||
and 1 on the back, but I’m helping to our Backend Engineer to deploy on GCP to
|
||||
production the API and DB, and I'm guiding the new Frontend to deliver new
|
||||
features, he’d developed the responsive design and some features to complement
|
||||
the task cards.
|
||||
|
||||
I’m happy to test my skills in this project, it’s not easy to take more
|
||||
responsibility with less than 2 years of labor experience, and it’d help me to
|
||||
grow a lot in these months.
|
||||
|
||||
Even if the market doesn’t consider my years of experience as a senior, I think
|
||||
that doesn’t matter at all, the only thing that matters is that you can
|
||||
understand why you’re using code, to create solutions and reach people across
|
||||
their computers.
|
29
src/content/videos/nadie-entiende-la-privacidad.mdx
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Nadie Entiende la Privacidad
|
||||
description:
|
||||
Hablar de privacidad es complicado, ya que no todo el mundo la entiende de
|
||||
verdad.
|
||||
tags:
|
||||
- Tech
|
||||
- Freedom
|
||||
- Libre
|
||||
image: https://img.youtube.com/vi/Wlw6rscU4gI/maxresdefault.jpg
|
||||
imageCaption: Video thumbnail.
|
||||
date: 6/3/2024
|
||||
author: Juan Manzanero
|
||||
rss: true
|
||||
---
|
||||
|
||||
<iframe
|
||||
width='100%'
|
||||
height='320'
|
||||
className='rounded-md'
|
||||
src='https://www.youtube-nocookie.com/embed/_x6SCSz7g5I'
|
||||
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.
|
||||
|
||||
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.
|
2
src/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
36
src/layouts/Layout.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
import Footer from "@/components/footer";
|
||||
import Navbar from "@/components/navbar.astro";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title, description, lang } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang || "en"}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link
|
||||
rel="alternate"
|
||||
title="juancmandev"
|
||||
type="application/rss+xml"
|
||||
href={new URL("rss.xml", Astro.site)}
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<Navbar />
|
||||
<main class="px-4 min-h-screen max-w-[65ch] py-32 mx-auto">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
28
src/pages/[...slug].astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import components from "@/components/mdx/wrapper";
|
||||
|
||||
interface Props {
|
||||
page: CollectionEntry<"pages">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPages = await getCollection("pages");
|
||||
|
||||
return allPages.map((page) => ({
|
||||
params: { slug: page.slug },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const { Content } = await page.render();
|
||||
---
|
||||
|
||||
<Layout title={page.data.title} description={page.data.description}>
|
||||
<article class="prose prose-invert">
|
||||
<Content components={{ ...components }} />
|
||||
</article>
|
||||
</Layout>
|
38
src/pages/blog/[...slug].astro
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import components from "@/components/mdx/wrapper";
|
||||
import formatDate from "@/utils/format-date";
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allBlogPosts = await getCollection(
|
||||
"blog",
|
||||
({ data }) => data.draft !== true,
|
||||
);
|
||||
|
||||
return allBlogPosts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<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>Posted: </strong>
|
||||
{post.data.date && formatDate(new Date(post.data.date))}
|
||||
</p>
|
||||
</article>
|
||||
</Layout>
|
32
src/pages/blog/index.astro
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
import PostItem from "@/components/post-item";
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||
allPosts.sort(
|
||||
(a, b) =>
|
||||
Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString()),
|
||||
);
|
||||
---
|
||||
|
||||
<Layout title="Blog" description="Check my projects.">
|
||||
<section class="prose prose-invert">
|
||||
<h1>Blog</h1>
|
||||
<p>Long format about thoughts and other topics.</p>
|
||||
</section>
|
||||
<ul class="mt-4 flex flex-col gap-4">
|
||||
{
|
||||
allPosts.map((blogpost) => (
|
||||
<li>
|
||||
<PostItem
|
||||
type="blog"
|
||||
slug={blogpost.slug}
|
||||
date={blogpost.data.date!}
|
||||
title={blogpost.data.title!}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Layout>
|
95
src/pages/index.astro
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import LinkButton from "@/components/link-button";
|
||||
import { getCollection } from "astro:content";
|
||||
import PostItem from "@/components/post-item";
|
||||
import { Image } from "astro:assets";
|
||||
import logo from "@/assets/logo.png";
|
||||
|
||||
const allPosts = await getCollection("blog", ({ data }) => data.draft !== true);
|
||||
allPosts.sort(
|
||||
(a, b) =>
|
||||
Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString()),
|
||||
);
|
||||
const last3Blogs = allPosts.slice(0, 3);
|
||||
|
||||
const allProjects = await getCollection(
|
||||
"portfolio",
|
||||
({ data }) => data.draft !== true,
|
||||
);
|
||||
allProjects.sort(
|
||||
(a, b) =>
|
||||
Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString()),
|
||||
);
|
||||
const last3Projects = allProjects.slice(0, 3);
|
||||
---
|
||||
|
||||
<Layout title="juancmandev" description="Welcome to my domain, stranger.">
|
||||
<div class="space-y-16">
|
||||
<section class="flex flex-col items-start gap-5">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-bold text-primary">
|
||||
Welcome to my domain, stranger.
|
||||
</h1>
|
||||
<h2 class="text-xl">
|
||||
I am <span class="font-semibold text-primary"
|
||||
>juancmandev</span
|
||||
>.
|
||||
</h2>
|
||||
<p class="text-lg font-light">
|
||||
I like computers, and all stuff related to technology.
|
||||
</p>
|
||||
<p class="text-lg font-light">
|
||||
Take a seat, drink some tea, and enjoy your stay.
|
||||
</p>
|
||||
</div>
|
||||
<Image
|
||||
src={logo}
|
||||
width={160}
|
||||
height={160}
|
||||
alt="juancmandev logo"
|
||||
class="w-40 h-40 aspect-square"
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-3xl">Latest posts</h2>
|
||||
<ul class="mt-4 flex flex-col gap-4">
|
||||
{
|
||||
last3Blogs.map((blogpost) => (
|
||||
<li>
|
||||
<PostItem
|
||||
type="blog"
|
||||
slug={blogpost.slug}
|
||||
date={blogpost.data.date!}
|
||||
title={blogpost.data.title!}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<LinkButton variant="secondary" href="/blog" className="mt-8"
|
||||
>More posts</LinkButton
|
||||
>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="text-3xl">Latest projects</h2>
|
||||
<ul class="mt-4 flex flex-col gap-4">
|
||||
{
|
||||
last3Projects.map((project) => (
|
||||
<li>
|
||||
<PostItem
|
||||
type="portfolio"
|
||||
slug={project.slug}
|
||||
date={project.data.date!}
|
||||
title={project.data.title!}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<LinkButton variant="secondary" href="/portfolio" className="mt-8"
|
||||
>More projects</LinkButton
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
33
src/pages/microblog.astro
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
export const prerender = false;
|
||||
|
||||
import MicroblogItem from "@/components/microblog-item";
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { createServerClient } from "@/utils/pocketbase";
|
||||
|
||||
const pb = createServerClient(import.meta.env.SECRET_POCKETBASE_API_URL);
|
||||
const data = await pb.collection("microblogs").getFullList({
|
||||
expand: "tags",
|
||||
sort: "-published",
|
||||
});
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Microblog"
|
||||
description="Short-format writing. Instead of using shitty social media."
|
||||
>
|
||||
<div class="prose prose-invert">
|
||||
<h1>Microblog</h1>
|
||||
<p>Short-format writing.</p>
|
||||
<p>Instead of using shitty social media.</p>
|
||||
<ul class="mx-auto p-0 mt-10 flex flex-col gap-10 list-none">
|
||||
{
|
||||
data.map((item: any) => (
|
||||
<li>
|
||||
<MicroblogItem {...item} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
38
src/pages/portfolio/[...slug].astro
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import components from "@/components/mdx/wrapper";
|
||||
import formatDate from "@/utils/format-date";
|
||||
|
||||
interface Props {
|
||||
project: CollectionEntry<"portfolio">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allProjects = await getCollection(
|
||||
"portfolio",
|
||||
({ data }) => data.draft !== true,
|
||||
);
|
||||
|
||||
return allProjects.map((project) => ({
|
||||
params: { slug: project.slug },
|
||||
props: { project },
|
||||
}));
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
const { Content } = await project.render();
|
||||
---
|
||||
|
||||
<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))}
|
||||
</p>
|
||||
</article>
|
||||
</Layout>
|
35
src/pages/portfolio/index.astro
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
import PostItem from "@/components/post-item";
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const allProjects = await getCollection(
|
||||
"portfolio",
|
||||
({ data }) => data.draft !== true,
|
||||
);
|
||||
allProjects.sort(
|
||||
(a, b) =>
|
||||
Date.parse(b.data.date.toString()) - Date.parse(a.data.date.toString()),
|
||||
);
|
||||
---
|
||||
|
||||
<Layout title="Blog" description="Long format about thoughts and other topics.">
|
||||
<section class="prose prose-invert">
|
||||
<h1 class="text-3xl font-bold">Portfolio</h1>
|
||||
<p>Check my projects.</p>
|
||||
</section>
|
||||
<ul class="mt-4 flex flex-col gap-4">
|
||||
{
|
||||
allProjects.map((project) => (
|
||||
<li>
|
||||
<PostItem
|
||||
type="portfolio"
|
||||
slug={project.slug}
|
||||
date={project.data.date!}
|
||||
title={project.data.title!}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Layout>
|
50
src/pages/rss.xml.js
Normal file
@ -0,0 +1,50 @@
|
||||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import MarkdownIt from "markdown-it";
|
||||
const parser = new MarkdownIt();
|
||||
|
||||
export async function GET(context) {
|
||||
const blog = await getCollection(
|
||||
"blog",
|
||||
({ data }) => data.draft !== true && data.rss === true,
|
||||
);
|
||||
const portfolio = await getCollection(
|
||||
"portfolio",
|
||||
({ data }) => data.draft !== true && data.rss === true,
|
||||
);
|
||||
|
||||
const blogItems = blog.map((post) => ({
|
||||
title: post.data.title,
|
||||
pubDate: post.data.date,
|
||||
description: post.data.description,
|
||||
tags: post.data.tags,
|
||||
author: post.data.author,
|
||||
link: `/blog/${post.slug}/`,
|
||||
content: sanitizeHtml(parser.render(post.body), {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||
}),
|
||||
}));
|
||||
|
||||
const portfolioItems = portfolio.map((project) => ({
|
||||
title: project.data.title,
|
||||
pubDate: project.data.date,
|
||||
description: project.data.description,
|
||||
tags: project.data.tags,
|
||||
author: project.data.author,
|
||||
link: `/portfolio/${project.slug}/`,
|
||||
content: sanitizeHtml(parser.render(project.body), {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||
}),
|
||||
}));
|
||||
|
||||
const items = [...blogItems, ...portfolioItems];
|
||||
|
||||
return rss({
|
||||
title: "juancmandev",
|
||||
description: "Welcome to my domain, stranger.",
|
||||
customData: `<language>en-us</language><lastBuildDate>${new Date()}</lastBuildDate>`,
|
||||
site: context.site,
|
||||
items,
|
||||
});
|
||||
}
|
36
src/styles/globals.css
Normal file
@ -0,0 +1,36 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
[data-rehype-pretty-code-figure] {
|
||||
@apply bg-[#1e1e2e] rounded-md pt-4;
|
||||
}
|
||||
[data-rehype-pretty-code-figure] figcaption {
|
||||
@apply m-0 mb-2 px-4 pb-4 border-b-[0.5px] border-secondary;
|
||||
}
|
||||
[data-rehype-pretty-code-figure] pre {
|
||||
@apply py-3 px-4;
|
||||
}
|
||||
[data-rehype-pretty-code-figure] pre > code > span {
|
||||
@apply pr-4;
|
||||
}
|
||||
[data-rehype-pretty-code-fragment] {
|
||||
@apply relative my-5 rounded-md border border-border/20 bg-[#282c34];
|
||||
}
|
||||
.prose code {
|
||||
@apply rounded-md border border-border/20 font-normal p-0.5 bg-secondary before:content-none after:content-none;
|
||||
}
|
||||
.prose pre > code {
|
||||
@apply bg-transparent border-none;
|
||||
}
|
||||
* {
|
||||
scroll-margin-top: 72px;
|
||||
}
|
||||
}
|
23
src/utils/format-date.ts
Normal file
@ -0,0 +1,23 @@
|
||||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
export default function formatDate(date: Date | string) {
|
||||
const newDate = new Date(date);
|
||||
const month = months[newDate.getMonth()];
|
||||
const day = newDate.getDate();
|
||||
const year = newDate.getFullYear();
|
||||
|
||||
return `${month} ${day}, ${year}`;
|
||||
}
|
36
src/utils/nav-links.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
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",
|
||||
},
|
||||
];
|
84
src/utils/pocketbase.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import PocketBase from "pocketbase";
|
||||
import type { RecordService } from "pocketbase";
|
||||
|
||||
export enum Collections {
|
||||
Microblogs = "microblogs",
|
||||
Tags = "tags",
|
||||
Users = "users",
|
||||
}
|
||||
|
||||
export type IsoDateString = string;
|
||||
export type RecordIdString = string;
|
||||
export type HTMLString = string;
|
||||
|
||||
export type BaseSystemFields<T = never> = {
|
||||
id: RecordIdString;
|
||||
created: IsoDateString;
|
||||
updated: IsoDateString;
|
||||
collectionId: string;
|
||||
collectionName: Collections;
|
||||
expand?: T;
|
||||
};
|
||||
|
||||
export type AuthSystemFields<T = never> = {
|
||||
email: string;
|
||||
emailVisibility: boolean;
|
||||
username: string;
|
||||
verified: boolean;
|
||||
} & BaseSystemFields<T>;
|
||||
|
||||
export type MicroblogsRecord = {
|
||||
content?: string;
|
||||
published: IsoDateString;
|
||||
tags?: RecordIdString[];
|
||||
};
|
||||
|
||||
export type TagsRecord = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type UsersRecord = {
|
||||
avatar?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type MicroblogsResponse<Texpand = unknown> = Required<MicroblogsRecord> &
|
||||
BaseSystemFields<Texpand>;
|
||||
export type TagsResponse<Texpand = unknown> = Required<TagsRecord> &
|
||||
BaseSystemFields<Texpand>;
|
||||
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> &
|
||||
AuthSystemFields<Texpand>;
|
||||
|
||||
export type CollectionRecords = {
|
||||
microblogs: MicroblogsRecord;
|
||||
tags: TagsRecord;
|
||||
users: UsersRecord;
|
||||
};
|
||||
|
||||
export type CollectionResponses = {
|
||||
microblogs: MicroblogsResponse;
|
||||
tags: TagsResponse;
|
||||
users: UsersResponse;
|
||||
};
|
||||
|
||||
export type TypedPocketBase = PocketBase & {
|
||||
collection(idOrName: "microblogs"): RecordService<MicroblogsResponse>;
|
||||
collection(idOrName: "tags"): RecordService<TagsResponse>;
|
||||
collection(idOrName: "users"): RecordService<UsersResponse>;
|
||||
};
|
||||
|
||||
export function createServerClient(url: string) {
|
||||
if (!url) {
|
||||
throw new Error("Pocketbase API url not defined !");
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
throw new Error(
|
||||
"This method is only supposed to call from the Server environment",
|
||||
);
|
||||
}
|
||||
|
||||
const client = new PocketBase(url) as TypedPocketBase;
|
||||
|
||||
return client;
|
||||
}
|
61
tailwind.config.mjs
Normal file
@ -0,0 +1,61 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
prefix: "",
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["Helvetica", "Arial", "sans-serif"],
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "#eee",
|
||||
input: "#00adb5",
|
||||
ring: "#00adb5",
|
||||
background: "#222831",
|
||||
foreground: "#eee",
|
||||
primary: {
|
||||
DEFAULT: "#00adb5",
|
||||
foreground: "#000",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "#393e46",
|
||||
foreground: "#eee",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "#ff2e63",
|
||||
foreground: "#eee",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "#393e46",
|
||||
foreground: "#eee",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "#00adb5",
|
||||
foreground: "#eee",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "#393e46",
|
||||
foreground: "#eee",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "#393e46",
|
||||
foreground: "#eee",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "8px",
|
||||
md: "4px",
|
||||
sm: "2px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
};
|
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|