diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357a..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..c85fb67c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index 62e62986..9f2955c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "zod": "3.23.8" }, "devDependencies": { + "@eslint/eslintrc": "3.2.0", "@types/jest": "29.5.14", "@types/mdx": "2.0.13", "@types/node": "22.9.0", @@ -806,9 +807,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 99dc52f4..7ac25329 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "zod": "3.23.8" }, "devDependencies": { + "@eslint/eslintrc": "3.2.0", "@types/jest": "29.5.14", "@types/mdx": "2.0.13", "@types/node": "22.9.0", diff --git a/public/assets/images/testimonials/alberto-chamorro.jpeg b/public/assets/images/testimonials/alberto-chamorro.jpeg new file mode 100644 index 00000000..cbab1c52 Binary files /dev/null and b/public/assets/images/testimonials/alberto-chamorro.jpeg differ diff --git a/public/assets/images/testimonials/antonio-cobo.jpeg b/public/assets/images/testimonials/antonio-cobo.jpeg new file mode 100644 index 00000000..a98acbe5 Binary files /dev/null and b/public/assets/images/testimonials/antonio-cobo.jpeg differ diff --git a/public/assets/images/testimonials/carlos-ble.jpeg b/public/assets/images/testimonials/carlos-ble.jpeg new file mode 100644 index 00000000..7a761ce4 Binary files /dev/null and b/public/assets/images/testimonials/carlos-ble.jpeg differ diff --git a/public/assets/images/testimonials/francisco-picolini.jpeg b/public/assets/images/testimonials/francisco-picolini.jpeg new file mode 100644 index 00000000..d099b500 Binary files /dev/null and b/public/assets/images/testimonials/francisco-picolini.jpeg differ diff --git a/public/assets/images/testimonials/german-delgado.jpeg b/public/assets/images/testimonials/german-delgado.jpeg new file mode 100644 index 00000000..5e9aa18d Binary files /dev/null and b/public/assets/images/testimonials/german-delgado.jpeg differ diff --git a/public/assets/images/testimonials/isabel-rodriguez.jpeg b/public/assets/images/testimonials/isabel-rodriguez.jpeg new file mode 100644 index 00000000..2e1b540e Binary files /dev/null and b/public/assets/images/testimonials/isabel-rodriguez.jpeg differ diff --git a/src/app/blog/(posts)/blog-redesign/page.mdx b/src/app/blog/(posts)/blog-redesign/page.mdx index 69b0bd6a..c865c910 100644 --- a/src/app/blog/(posts)/blog-redesign/page.mdx +++ b/src/app/blog/(posts)/blog-redesign/page.mdx @@ -26,7 +26,7 @@ A powerful way of conveying your message—whatever that might be—is to make s ## Technologies -Technology *needs* to serve a purpose. Sometimes—most times, let's be honest—we web developers focus too much on the technologies and frameworks we use. We should talk about them, be proud of what we use, and maybe even poke fun at others' choices, but only to a *certain* extent. Here, I'm mentioning some of the technologies I've used, what I've learned from them, and how they've empowered me to focus on what really matters: **the message**. +Technology _needs_ to serve a purpose. Sometimes—most times, let's be honest—we web developers focus too much on the technologies and frameworks we use. We should talk about them, be proud of what we use, and maybe even poke fun at others' choices, but only to a _certain_ extent. Here, I'm mentioning some of the technologies I've used, what I've learned from them, and how they've empowered me to focus on what really matters: **the message**. - Next.js @@ -59,7 +59,7 @@ When using a starter, what I do is generate the template in a temporary folder a I also configured the project as I usually do, with [Prettier](https://prettier.io/), [ESLint](https://eslint.org/), [Husky](https://typicode.github.io/husky/), and [Lint Staged](https://github.com/lint-staged/lint-staged). How I configure the projects I work on could definitely be another post at some point. -I made a conscious—perhaps also debatable—choice not to delve too deep into **best practices**, **testing**, and **architecture**. _Why_? Because I want to create a blog series where I can improve the codebase bit by *byte*. But hey, it's not like I coded the *thing* with my eyes closed (which could be an interesting challenge, to be honest). You can check the source code [here](https://github.com/cesalberca/blog/tree/6037908cf67122706665b3d8c00590bd0a5f5040) at the time of publishing this post. I think it's pretty neat. +I made a conscious—perhaps also debatable—choice not to delve too deep into **best practices**, **testing**, and **architecture**. _Why_? Because I want to create a blog series where I can improve the codebase bit by _byte_. But hey, it's not like I coded the _thing_ with my eyes closed (which could be an interesting challenge, to be honest). You can check the source code [here](https://github.com/cesalberca/blog/tree/6037908cf67122706665b3d8c00590bd0a5f5040) at the time of publishing this post. I think it's pretty neat. What I decided to focus on instead was creating a clean design with a big focus on content and some subtle animations that would make the site b·e·a·u·t·i·f·u·l. Here are some: @@ -91,14 +91,15 @@ What I decided to focus on instead was creating a clean design with a big focus + -I'm always on the lookout for inspiration. Seeing how others have tackled similar problems reassures me that I'm not alone in my quest for *my solution*. Some great places I stole ideas from looked for inspiration were [Codrops](https://tympanus.net/codrops/) and [Awwwards](https://www.awwwards.com/). +I'm always on the lookout for inspiration. Seeing how others have tackled similar problems reassures me that I'm not alone in my quest for _my solution_. Some great places I stole ideas from looked for inspiration were [Codrops](https://tympanus.net/codrops/) and [Awwwards](https://www.awwwards.com/). ## MDX -As you can see, all the components I'm developing for the website can be reused in blog posts, and that's thanks to [MDX](https://mdxjs.com/). MDX is like Markdown on Kombucha (meaning *super powerful*). Why? Because in **MDX, you can import React components**, which is a crazy thing to do. +As you can see, all the components I'm developing for the website can be reused in blog posts, and that's thanks to [MDX](https://mdxjs.com/). MDX is like Markdown on Kombucha (meaning _super powerful_). Why? Because in **MDX, you can import React components**, which is a crazy thing to do. ```mdx **Foo** @@ -122,7 +123,7 @@ Which will render this: Cool, right? -Next.js comes with support for MDX using some extra libraries. I started configuring the project following the [Next.js MDX guide](https://nextjs.org/docs/pages/building-your-application/configuring/mdx), which at some point suggests using [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote). I found it a little cumbersome and noticed that it provided some functionality I don't really need (*YAGNI*). +Next.js comes with support for MDX using some extra libraries. I started configuring the project following the [Next.js MDX guide](https://nextjs.org/docs/pages/building-your-application/configuring/mdx), which at some point suggests using [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote). I found it a little cumbersome and noticed that it provided some functionality I don't really need (_YAGNI_). After searching for simpler solutions, I stumbled upon [this fantastic post](https://www.alexchantastic.com/building-a-blog-with-next-and-mdx), which showed how to simplify the setup using just [next-mdx](https://www.npmjs.com/package/@next/mdx). @@ -316,7 +317,7 @@ This removes the need of adding `front-matter` and a subsequent parser. **Simple ## TaildwindCSS -In case you missed it, there's some code that might initially look **aberrant**. If so, it's because you haven't tried *yet* [TailwindCSS](https://tailwindcss.com/): +In case you missed it, there's some code that might initially look **aberrant**. If so, it's because you haven't tried _yet_ [TailwindCSS](https://tailwindcss.com/): ```tsx @@ -394,7 +395,7 @@ export default config ## ShadcnUI -ShadcnUI introduces a concept that might initially make us shudder in fear: copying and pasting code. It does this by generating code for your components. But why would I want that? Well, it serves as a base for lightweight components, and from there, I can customize them as much as I want. The code is *mine* after all. +ShadcnUI introduces a concept that might initially make us shudder in fear: copying and pasting code. It does this by generating code for your components. But why would I want that? Well, it serves as a base for lightweight components, and from there, I can customize them as much as I want. The code is _mine_ after all. Whenever I need a new component, I head [over to their documentation](https://ui.shadcn.com/docs) and find the component I need. Then, I generate it using the CLI: diff --git a/src/app/blog/(posts)/create-angular-17-project/page.mdx b/src/app/blog/(posts)/create-angular-17-project/page.mdx index 6d3e1649..8c994f05 100644 --- a/src/app/blog/(posts)/create-angular-17-project/page.mdx +++ b/src/app/blog/(posts)/create-angular-17-project/page.mdx @@ -27,9 +27,9 @@ With the SSR option enabled, Angular will create two execution environments: cli If we examine the project structure, we will see that it has created a series of configuration files for both the client and server parts. Here's a summary: -* main.ts ➜ main.server.ts -* app.config.ts ➜ app.config.server.ts -* server.ts (Server-only file) +- main.ts ➜ main.server.ts +- app.config.ts ➜ app.config.server.ts +- server.ts (Server-only file) ### `server.ts` diff --git a/src/app/blog/_drafts/2024-a-review-of-a-transformative-year/page.mdx b/src/app/blog/_drafts/2024-a-review-of-a-transformative-year/page.mdx index 49a1ebf2..210cef72 100644 --- a/src/app/blog/_drafts/2024-a-review-of-a-transformative-year/page.mdx +++ b/src/app/blog/_drafts/2024-a-review-of-a-transformative-year/page.mdx @@ -21,7 +21,7 @@ A raíz de la baja comencé a documentar mis dolores. Leyendo mis diarios y nota Tenía que adentrarme en la desesperación y en el sentirme acorralado por mis achaques para que mi **motivación fuese inquebrantable**. Siempre he sido una persona (pro)activa, que lucha incesantemente y es que esa lucha continua era parte del problema. Necesitaba dejar ir. Y no era una rendición, si no aceptación. -Así que sin saber qué construir decidé reconstruirme. Gimnasio, natación, escalada, calistenia y pilates fueron las herramientas que elegí. Empecé con peso muerto. 60 Kg estremecían y llevaban a mis músculos al límite. Y luego fueron 80 kg, 100 kg, 120 kg y... 150 kg, el doble de mi peso corporal. La medicación recetada por mi reumatólogo para aumentar mi serotonina decidí complementarla con meditaciones diarias. Unos 20-40 minutos, usando Headspace. A día de hoy llevo más de 80 horas totales meditando. +Así que sin saber qué construir decidé reconstruirme. Gimnasio, natación, escalada, calistenia y pilates fueron las herramientas que elegí. Empecé con peso muerto. 60 Kg estremecían y llevaban a mis músculos al límite. Y luego fueron 80 kg, 100 kg, 120 kg y... 150 kg, el doble de mi peso corporal. La medicación recetada por mi reumatólogo para aumentar mi serotonina decidí complementarla con meditaciones diarias. Unos 20-40 minutos, usando Headspace. A día de hoy llevo más de 80 horas totales meditando. Hallé una nueva psicóloga con la que he estado y estoy trabajando un montón de heridas emocionales que de alguna forma afectan a mis dolores físicos. Aunque no he bebido nunca mucho empecé a sustituirlo por agua con gas. Intenté mejorar mi alimentación, aunque no ha sido a finales de 2024 donde ya he refinado mucho más mi nutrición. Daba paseos diarios, tomando el sol e intentando practicar mindfulness. No todos los días hacía todo. Algún día fallaba, pero nunca nunca podía fallar dos días seguidos. Y... empecé a ver cambios. Mi mente no parecía tan nublada, empecé a conectar con emociones las cuales eran muy ajenas a mí, como la rabia o la tristeza, ya que llevaba muchos años disociado. Empecé a cambiar el debo por el quiero. Empecé a priorizarme. En estos primeros meses de 2024 tomé dos decisiones: debía cortar con mi actual pareja y dejar la empresa para la que había trabajado 7 años como desarrollador de Software. Dos decisiones que me costaron mucho ya que han constituido una parte esencial de quien soy como persona. @@ -64,12 +64,7 @@ En el coliving creé contenido y aproveché a practicar asiduamente con la nueva Volví a Madrid. Escalé. Escalé. Escalé. ¡Mi primer 7A! - - - - - - +¿Y cómo me encuentro? Bien. No es un bien desde no sé cómo me encuentro o un bien desde la disociación. O desde está el perdido. O desde el querer enmascarar el dolor para no hacer sufrir de forma innecesaria a quien pregunte. export default function Page({ children }) { return {children} diff --git a/src/app/globals.css b/src/app/globals.css index f73277e0..c310f776 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -81,6 +81,7 @@ html { h6 { @apply leading-tight; font-family: var(--font-azeret-mono); + scroll-margin-top: 5rem; } h1 { diff --git a/src/app/talks/(talks)/advanced-javascript-patterns/page.mdx b/src/app/talks/(talks)/advanced-javascript-patterns/page.mdx index 7f7f8d70..f4058cbb 100644 --- a/src/app/talks/(talks)/advanced-javascript-patterns/page.mdx +++ b/src/app/talks/(talks)/advanced-javascript-patterns/page.mdx @@ -1,50 +1,47 @@ import Layout from '../../mdx-layout' export const metadata = { - title: "Advanced JavaScript Patterns", + title: 'Advanced JavaScript Patterns', length: 45, - slug: "advanced-javascript-patterns", - difficulty: "Advanced", - image: "advanced-javascript-patterns.png", - categories: [ - "software-development", - "javascript" - ], + slug: 'advanced-javascript-patterns', + difficulty: 'Advanced', + image: 'advanced-javascript-patterns.png', + categories: ['software-development', 'javascript'], events: [ { - name: "JSDay Madrid", - date: "2018-10-20", - slides: "https://drive.google.com/open?id=15tLH3xBpZbwHlhRGtiygc0x9NRaxNlPWhqghw5iNu4U", - code: "https://github.com/cesalberca/advanced-javascript-patterns/tree/2018-jsday-madrid", - video: "https://www.youtube.com/watch?v=aNf1Oos0ZB8" + name: 'JSDay Madrid', + date: '2018-10-20', + slides: 'https://drive.google.com/open?id=15tLH3xBpZbwHlhRGtiygc0x9NRaxNlPWhqghw5iNu4U', + code: 'https://github.com/cesalberca/advanced-javascript-patterns/tree/2018-jsday-madrid', + video: 'https://www.youtube.com/watch?v=aNf1Oos0ZB8', }, { - name: "JSDay Canarias", - date: "2019-11-10", - slides: "https://drive.google.com/open?id=1Eoa1EVelW11x5Es7Ru0tnmWdzfQOAlEWz07TK1qLh_o", - code: "https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-jsday-canarias", - video: "https://youtu.be/3nPPRvRcoo0" + name: 'JSDay Canarias', + date: '2019-11-10', + slides: 'https://drive.google.com/open?id=1Eoa1EVelW11x5Es7Ru0tnmWdzfQOAlEWz07TK1qLh_o', + code: 'https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-jsday-canarias', + video: 'https://youtu.be/3nPPRvRcoo0', }, { - name: "Codenares", - date: "2019-03-19", - slides: "https://drive.google.com/open?id=1LEWc3ErrzjaN3nzlsMtGVx1QKGsc8G2mgw-qP-WZhJo", - code: "https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-codenares", - video: "https://youtu.be/MuCgyOTXIRU" + name: 'Codenares', + date: '2019-03-19', + slides: 'https://drive.google.com/open?id=1LEWc3ErrzjaN3nzlsMtGVx1QKGsc8G2mgw-qP-WZhJo', + code: 'https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-codenares', + video: 'https://youtu.be/MuCgyOTXIRU', }, { - name: "OS Weekends", - date: "2019-12-13", - slides: "https://docs.google.com/presentation/d/1D0Xe0Ch5eGcy-CDLwxpW_QwoDX6-mGxjprWmQ_EkH5c/edit?usp=sharing", - code: "https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-osweekends" + name: 'OS Weekends', + date: '2019-12-13', + slides: 'https://docs.google.com/presentation/d/1D0Xe0Ch5eGcy-CDLwxpW_QwoDX6-mGxjprWmQ_EkH5c/edit?usp=sharing', + code: 'https://github.com/cesalberca/advanced-javascript-patterns/tree/2019-osweekends', }, { - name: "Codemotion Meetup", - date: "2020-02-25", - slides: "https://docs.google.com/presentation/d/1htHNH9BfNdXmSn88eJNrLzeys6Het9i6EUqBn-v_01U/edit?usp=sharing", - code: "https://github.com/cesalberca/advanced-javascript-patterns/tree/2020-codemotion-meetup-madrid" - } - ] + name: 'Codemotion Meetup', + date: '2020-02-25', + slides: 'https://docs.google.com/presentation/d/1htHNH9BfNdXmSn88eJNrLzeys6Het9i6EUqBn-v_01U/edit?usp=sharing', + code: 'https://github.com/cesalberca/advanced-javascript-patterns/tree/2020-codemotion-meetup-madrid', + }, + ], } When you tell people you code in JavaScript they usually look down on you. This talk will give you the tools to enlighten those heretics. We'll be entering the world of **design patterns** applied to JavaScript, looking through some of the cutting edge properties of ES6 such as **Proxies** and **default function parameter expressions**, dealing with **objects** and even some examples of **functional programming**. This talk is not allowed for cardiacs. diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 00000000..97c29ebd --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,40 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 00000000..6a21dc2c --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,226 @@ +'use client' + +import * as React from 'react' +import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react' +import { ArrowLeft, ArrowRight } from 'lucide-react' + +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' + +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin + orientation?: 'horizontal' | 'vertical' + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: boolean + canScrollNext: boolean +} & CarouselProps + +const CarouselContext = React.createContext(null) + +function useCarousel() { + const context = React.useContext(CarouselContext) + + if (!context) { + throw new Error('useCarousel must be used within a ') + } + + return context +} + +const Carousel = React.forwardRef & CarouselProps>( + ({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins, + ) + const [canScrollPrev, setCanScrollPrev] = React.useState(false) + const [canScrollNext, setCanScrollNext] = React.useState(false) + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return + } + + setCanScrollPrev(api.canScrollPrev()) + setCanScrollNext(api.canScrollNext()) + }, []) + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev() + }, [api]) + + const scrollNext = React.useCallback(() => { + api?.scrollNext() + }, [api]) + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault() + scrollPrev() + } else if (event.key === 'ArrowRight') { + event.preventDefault() + scrollNext() + } + }, + [scrollPrev, scrollNext], + ) + + React.useEffect(() => { + if (!api || !setApi) { + return + } + + setApi(api) + }, [api, setApi]) + + React.useEffect(() => { + if (!api) { + return + } + + onSelect(api) + api.on('reInit', onSelect) + api.on('select', onSelect) + + return () => { + api?.off('select', onSelect) + } + }, [api, onSelect]) + + return ( + +
+ {children} +
+
+ ) + }, +) +Carousel.displayName = 'Carousel' + +const CarouselContent = React.forwardRef>( + ({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) + }, +) +CarouselContent.displayName = 'CarouselContent' + +const CarouselItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const { orientation } = useCarousel() + + return ( +
+ ) + }, +) +CarouselItem.displayName = 'CarouselItem' + +const CarouselPrevious = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) + }, +) +CarouselPrevious.displayName = 'CarouselPrevious' + +const CarouselNext = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) + }, +) +CarouselNext.displayName = 'CarouselNext' + +export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 50d09425..bd234697 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { cn } from '@/lib/utils' +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef(({ className, type, ...props }, ref) => { diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 05100c63..348362a2 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { cn } from '@/lib/utils' +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface TextareaProps extends React.TextareaHTMLAttributes {} const Textarea = React.forwardRef(({ className, ...props }, ref) => { diff --git a/src/core/components/background/background.tsx b/src/core/components/background/background.tsx index c3639521..3a640c47 100644 --- a/src/core/components/background/background.tsx +++ b/src/core/components/background/background.tsx @@ -1,7 +1,7 @@ 'use client' import React, { type FC, type MouseEvent, type PropsWithChildren, useRef } from 'react' -import { motion, useAnimation, useMotionValue, useMotionValueEvent, useScroll } from 'framer-motion' +import { motion, useAnimation, useMotionValue, useScroll } from 'framer-motion' import { linearInterpolation } from '@/core/3d/linear-interpolation' import { getRandomString } from '@/core/utils/get-random-string' import { cn } from '@/lib/utils' diff --git a/src/core/components/markdown/markdown.tsx b/src/core/components/markdown/markdown.tsx index 930cad1a..4dbe6334 100644 --- a/src/core/components/markdown/markdown.tsx +++ b/src/core/components/markdown/markdown.tsx @@ -10,7 +10,7 @@ export const Markdown: FC<{ value: string; className?: string }> = async ({ valu className={cn('prose prose-zinc dark:prose-invert text-current', className)} components={{ code(props) { - const { children, className, node, ...rest } = props + const { children, className, ...rest } = props const match = /language-(\w+)/.exec(className ?? '') return match ? ( diff --git a/src/core/components/navbar/navbar.tsx b/src/core/components/navbar/navbar.tsx index b080b868..8c282e22 100644 --- a/src/core/components/navbar/navbar.tsx +++ b/src/core/components/navbar/navbar.tsx @@ -11,8 +11,13 @@ const Links = () => { const t = useTranslations() return ( <> + {t('home.projects.title')} + {t('home.services.title')} + {t('home.experience.title')} {t('blog.title')} {t('talks.title')} + {t('home.testimonials.title')} + {t('home.contact.title')} ) } diff --git a/src/core/components/testimonials/testimonials.tsx b/src/core/components/testimonials/testimonials.tsx new file mode 100644 index 00000000..14f41be8 --- /dev/null +++ b/src/core/components/testimonials/testimonials.tsx @@ -0,0 +1,143 @@ +'use client' + +import React, { type FC } from 'react' +import { Card, CardContent } from '@/components/ui/card' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel' +import { Link } from '@/core/components/link/link' + +interface Testimonial { + name: string + role: string + link: string + content: string + avatar: string +} + +const testimonials: Testimonial[] = [ + { + name: 'Isabel Rodríguez', + role: 'Business Director, Next Digital', + link: 'https://www.linkedin.com/in/isabel-rodriguez-olivar-573124b/', + content: `I had the pleasure of meeting César when he was just starting his career—very young but full of energy and eager to learn. From the beginning, he stood out for his commitment and enthusiasm for sharing his knowledge with others, qualities that have always been his greatest strengths. + +Over time, César has become a respected figure in the industry, thanks to his continuous training and dedication to developing high-quality software. He has always been willing to take on every task and project I proposed to him, approaching them with a proactive and collaborative attitude. His ability to take on challenges, along with his generosity in sharing what he knows, make him a valuable professional and an excellent colleague.`, + avatar: '/assets/images/testimonials/isabel-rodriguez.jpeg', + }, + { + name: 'Germán Delgado García', + role: 'Software Developer, Autentia', + link: 'https://www.linkedin.com/in/germandelgadogarcia/', + content: + 'I have had the privilege of working with César, who is a software development reference. His deep knowledge of software architecture and design is impressive. I am very grateful to have had the opportunity to learn a lot from him, both on the job and in the conferences he has given.\n' + + '\n' + + 'César is curious, funny and intelligent. What stands out most about him is his generosity in sharing his knowledge and experience with others. He is not only a highly skilled professional, but also a person who inspires continuous growth and improvement.\n' + + '\n' + + 'If you are looking for someone who combines great technical skills with the ability to communicate and teach clearly and effectively, César is the person for you. I am sure he will be a valuable addition to any team.', + avatar: '/assets/images/testimonials/german-delgado.jpeg', + }, + { + name: 'Francisco Picolini', + role: 'Open Source Community Manager, OpenNebula', + link: 'https://www.linkedin.com/in/francjp/', + content: + 'I had the pleasure of collaborating with César during my time managing the Conference content for Codemotion Madrid. César played a crucial role in shaping the agenda, particularly by selecting top-notch frontend proposals, which is his area of expertise. He also contributed beyond this by suggesting innovative topics and recommending talented speakers.\n' + + '\n' + + 'Not only is César a highly engaging and skilled speaker, but he is also a dedicated team player who consistently goes above and beyond to ensure project success. His deep knowledge of modern frameworks and emerging technologies is impressive, and he has a genuine passion for sharing that expertise with others.\n' + + '\n' + + 'César is a complete developer—always well-informed, forward-thinking, and eager to contribute to the success of the team. He’s a true asset to any project or organization.', + avatar: '/assets/images/testimonials/francisco-picolini.jpeg', + }, + { + name: 'Alberto Chamorro', + link: 'https://www.linkedin.com/in/alberto-chamorro/', + role: 'Flutter Mobile Developer, Calisteniapp', + content: + 'César is a great person, developer, and colleague with whom I had the pleasure of working at Autentia. During the time I worked with him, he demonstrated strong capabilities in facing various technical challenges, consistently creating effective and creative solutions that always added significant value for the client.\n' + + '\n' + + 'I can confidently say that César possesses solid knowledge not only in the technological field but also in software architecture and design. Additionally, his hunger for knowledge, combined with his eagerness to share it, makes César a highly valuable asset for any team or challenge he takes on.', + avatar: '/assets/images/testimonials/alberto-chamorro.jpeg', + }, + { + name: 'Carlos Blé', + link: 'https://www.linkedin.com/in/carlosble/', + role: 'Life and Executive Coach, LeanMind', + content: + 'César is a kind and friendly person with a great sense of humor, and, of course, he is an excellent professional. Highly intelligent, very capable, and extremely hardworking. Years ago, I had the chance to have him as an outstanding student in several of my courses, and I was impressed by his ability and eagerness to learn. César is a professional with common sense and exceptional technical quality.', + avatar: '/assets/images/testimonials/carlos-ble.jpeg', + }, + { + name: 'Antonio Cobo Cuenca', + role: 'Agile Coach, Cognizant', + link: 'https://www.linkedin.com/in/antoniocobocuenca/', + content: + 'César and I served on a conference program committee together for several years. I consistently relied on his deep understanding of the latest trends in the Front End world. During our discussions about improving the conference, I was impressed by his critical thinking skills and his ability to see the big picture. César considers every angle of a problem and consistently finds the right solution.', + avatar: '/assets/images/testimonials/antonio-cobo.jpeg', + }, +] + +interface TestimonialsProps { + itemsPerPage?: number +} + +export const Testimonials: FC = ({ itemsPerPage = 1 } = { itemsPerPage: 1 }) => { + const getItemBasisClass = (itemsPerPage: number) => { + switch (itemsPerPage) { + case 1: + return 'md:basis-full' + case 2: + return 'md:basis-1/2' + case 3: + return 'md:basis-1/3' + case 4: + return 'md:basis-1/4' + default: + return 'md:basis-1/3' + } + } + + const itemBasisClass = getItemBasisClass(itemsPerPage) + + return ( +
+ + + {testimonials.map((testimonial, index) => ( + + + +
“{testimonial.content}”
+
+ + + + {testimonial.name + .split(' ') + .map(n => n[0]) + .join('')} + + +
+ + {{testimonial.name}} + +

{testimonial.role}

+
+
+
+
+
+ ))} +
+ + +
+
+ ) +} diff --git a/src/core/datetime.test.ts b/src/core/datetime.test.ts index 72317160..9bfe6636 100644 --- a/src/core/datetime.test.ts +++ b/src/core/datetime.test.ts @@ -35,7 +35,7 @@ describe.skip('Datetime', () => { it('should get now datetime', () => { const date = new Date(2019, 3, 3, 3, 3, 3, 3) - ;(global.Date as any) = jest.fn(() => date) + ;(global.Date as unknown) = jest.fn(() => date) const datetime = Datetime.fromNow() const actual = datetime.value diff --git a/src/core/datetime.ts b/src/core/datetime.ts index 98baa07b..c3c2e3fa 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -1,5 +1,3 @@ -import type { Locale } from './i18n/locale' - export class Datetime { private constructor(private readonly _value: Date) {} @@ -23,8 +21,7 @@ export class Datetime { return this._value.getFullYear() } - // TODO: Receive locale - format(_locale?: Locale) { + format() { return this._value.toLocaleDateString('en', { day: 'numeric', month: 'long', diff --git a/src/core/hooks/use-interval.ts b/src/core/hooks/use-interval.ts index 82018c7f..de75dcf8 100644 --- a/src/core/hooks/use-interval.ts +++ b/src/core/hooks/use-interval.ts @@ -12,7 +12,7 @@ export function useInterval(callback: () => void, delay: number | null) { savedCallback.current?.() } if (delay !== null) { - let id = setInterval(tick, delay) + const id = setInterval(tick, delay) return () => clearInterval(id) } }, [delay]) diff --git a/src/core/i18n/translations/en.json b/src/core/i18n/translations/en.json index 3c5ddd43..e6584d3b 100644 --- a/src/core/i18n/translations/en.json +++ b/src/core/i18n/translations/en.json @@ -31,7 +31,7 @@ "meAlt": "Picture of César Alberca", "technologies": { "title": "Technologies", - "description": "With over **5 years in ReactJS**, **4 in Angular**, and **2 in VueJS**, I've consistently used **TypeScript**, **Testing**, and architectural patterns like **DDD** and **Hexagonal** to enhance project maintainability.\n\nI stay current with new technologies, prioritizing best practices for long-term success. Here are some technologies I'm competent with:" + "description": "With over **5 years in ReactJS**, **4 in Angular**, and **2 in VueJS**, I've consistently used **TypeScript**, **Testing**, and architectural patterns like **DDD** and **Hexagonal** to enhance project maintainability." }, "latestArticles": "Latest articles", "viewAllArticles": "View all articles", @@ -117,6 +117,9 @@ "development": "Software Development", "developmentDescription": "I will develop your project from **design** through **development** to **deployment**." }, + "testimonials": { + "title": "Testimonials" + }, "contact": { "title": "Contact", "appointmentDescription": "Would you rather jump on a quick call to discuss your business needs?", diff --git a/src/features/home/delivery/contact.tsx b/src/features/home/delivery/contact.tsx index d33f2535..0b9a7356 100644 --- a/src/features/home/delivery/contact.tsx +++ b/src/features/home/delivery/contact.tsx @@ -71,7 +71,7 @@ export const ContactForm: FC = () => {

{t('home.contact.sent')}

) : (
- + { {experiences.map((x, i) => (
-
{x.title}
+

{x.title}

{x.dates}
diff --git a/src/features/home/delivery/home.page.tsx b/src/features/home/delivery/home.page.tsx index d848fcca..8417769b 100644 --- a/src/features/home/delivery/home.page.tsx +++ b/src/features/home/delivery/home.page.tsx @@ -12,16 +12,20 @@ import { AccentText } from '@/core/components/accent-text/accent-text' import { ContactForm } from '@/features/home/delivery/contact' import { OpenToWork } from '@/core/components/open-to-work/open-to-work' import { Experience } from '@/features/home/delivery/experience' +import { Testimonials } from '@/core/components/testimonials/testimonials' +import { cn } from '@/lib/utils' export const Section: FC< PropsWithChildren<{ title?: string + id?: string + fullWidth?: boolean }> -> = ({ children, title }) => { +> = ({ children, title, id, fullWidth = false }) => { return (
{title && ( -

+

{title}

)} @@ -62,28 +66,32 @@ export const HomePage: FC = () => {
-
+
-
+
-
+
-
+
-
+
+ +
+ +
diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx index f27b32d7..2f5b8253 100644 --- a/src/mdx-components.tsx +++ b/src/mdx-components.tsx @@ -84,6 +84,7 @@ function createHeading(level: number) { return Heading } +// eslint-disable-next-line @typescript-eslint/no-explicit-any const customComponents: ComponentProps['components'] = { h1: createHeading(1), h2: createHeading(2), diff --git a/tsconfig.json b/tsconfig.json index fbbaa89d..3c3582bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -9,7 +9,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "experimentalDecorators": true,