@@ -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')}
) : (