diff --git a/app/[lang]/components/mdx-content.tsx b/app/[lang]/components/mdx-content.tsx new file mode 100644 index 0000000..c43c1d8 --- /dev/null +++ b/app/[lang]/components/mdx-content.tsx @@ -0,0 +1,48 @@ +'use client'; + +import Markdown from 'markdown-to-jsx'; + +export function MDXContent({ children }: { children: string }) { + return ( + + {children} + + ); +} diff --git a/app/[lang]/components/navigation.tsx b/app/[lang]/components/navigation.tsx index f990263..9765b99 100644 --- a/app/[lang]/components/navigation.tsx +++ b/app/[lang]/components/navigation.tsx @@ -1,9 +1,7 @@ 'use client'; import { navigation } from '@/constants/navigation'; import { Locale } from '@/i18n-config'; -import { ArrowLeft } from 'lucide-react'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; import React, { useEffect, useRef, useState } from 'react'; interface Props { @@ -11,7 +9,6 @@ interface Props { } export const Navigation: React.FC = ({ lang }) => { - const router = useRouter(); const ref = useRef(null); const [isIntersecting, setIntersecting] = useState(true); useEffect(() => { @@ -45,13 +42,6 @@ export const Navigation: React.FC = ({ lang }) => { ))} - diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index ba5559f..b24608a 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -5,11 +5,12 @@ import { Analytics } from '@vercel/analytics/react'; import '../../globals.css'; import { englishMetadata, spanishMetadata } from '@/constants/metadata'; -export async function generateMetadata({ - params, -}: { - params: { lang: 'es' | 'en' }; -}) { +export async function generateMetadata( + props: { + params: Promise<{ lang: 'es' | 'en' }>; + } +) { + const params = await props.params; return params.lang === 'en' ? englishMetadata : spanishMetadata; } @@ -17,13 +18,18 @@ export async function generateStaticParams() { return i18n.locales.map(locale => ({ lang: locale })); } -export default function RootLayout({ - children, - params, -}: { - children: React.ReactNode; - params: { lang: string }; -}) { +export default async function RootLayout( + props: { + children: React.ReactNode; + params: Promise<{ lang: string }>; + } +) { + const params = await props.params; + + const { + children + } = props; + return ( diff --git a/app/[lang]/projects/[slug]/page.tsx b/app/[lang]/projects/[slug]/page.tsx index 380fa18..ff3c1e9 100644 --- a/app/[lang]/projects/[slug]/page.tsx +++ b/app/[lang]/projects/[slug]/page.tsx @@ -11,17 +11,18 @@ type PropsParams = { }; interface Props { - params: { + params: Promise<{ slug: string; lang: Locale; - }; + }>; } export async function generateStaticParams(): Promise { const projectsES = projects.es.map(project => ({ slug: project.slug })); const projectsEN = projects.en.map(project => ({ slug: project.slug })); return [...projectsEN, ...projectsES]; } -export default async function PostPage({ params }: Props) { +export default async function PostPage(props: Props) { + const params = await props.params; const slug = params?.slug; const project = projects[params.lang].find(project => project.slug === slug); @@ -48,9 +49,9 @@ export default async function PostPage({ params }: Props) { ? 'Algunas de las tecnologías que utilice en este proyecto:' : 'Some of the technologies used in this project'}

-
    +
      {project.stack.map(tec => ( -
    • +
    • {tec}
    • ))} diff --git a/app/[lang]/projects/layout.tsx b/app/[lang]/projects/layout.tsx index 0492736..13c6b55 100644 --- a/app/[lang]/projects/layout.tsx +++ b/app/[lang]/projects/layout.tsx @@ -5,13 +5,18 @@ export async function generateStaticParams() { return i18n.locales.map(locale => ({ lang: locale })); } -export default function ProjectsLayout({ - children, - params, -}: { - children: React.ReactNode; - params: { lang: string }; -}) { +export default async function ProjectsLayout( + props: { + children: React.ReactNode; + params: Promise<{ lang: string }>; + } +) { + const params = await props.params; + + const { + children + } = props; + return ( ({ lang: locale })); } -export default function ResumeLayout({ - children, - params, -}: { - children: React.ReactNode; - params: { lang: string }; -}) { +export default async function ResumeLayout( + props: { + children: React.ReactNode; + params: Promise<{ lang: string }>; + } +) { + const params = await props.params; + + const { + children + } = props; + return ( = ({ work }) => { + const ref = useRef(null); + const [isIntersecting, setIntersecting] = useState(true); + + const links: { label: string; href: string }[] = []; + if (work.url) { + links.push({ + label: 'Web', + href: work.url, + }); + } + + useEffect(() => { + if (!ref.current) return; + const observer = new IntersectionObserver(([entry]) => + setIntersecting(entry.isIntersecting) + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, []); + + return ( +
      +
      +
      +
      + {work.url && ( + + + + )} +
      + + + + +
      +
      +
      +
      +
      +

      + {work.title} +

      +

      {work.brief}

      +
      + +
      +
      + {links.map(link => ( + + {link.label} + + ))} +
      +
      +
      +
      +
      + ); +}; diff --git a/app/[lang]/works/[slug]/page.tsx b/app/[lang]/works/[slug]/page.tsx new file mode 100644 index 0000000..570117a --- /dev/null +++ b/app/[lang]/works/[slug]/page.tsx @@ -0,0 +1,63 @@ +import { format } from 'date-fns'; +import Image from 'next/image'; +import { MDXContent } from '@/app/[lang]/components/mdx-content'; +import { getWorkPost, getWorkPosts } from '@/util/posts/work'; +import { Header } from './header'; +import { es, enUS } from 'date-fns/locale'; + +export async function generateStaticParams({ + params: { lang }, +}: { + params: { lang: string }; +}) { + const posts = await getWorkPosts(lang); + return posts.map(post => ({ + slug: post.slug, + })); +} + +export default async function WorkPost(props: { + params: Promise<{ lang: string; slug: string }>; +}) { + const params = await props.params; + + const { lang, slug } = params; + + const post = await getWorkPost(lang, slug); + + return ( +
      +
      +
      + {post.image && ( +
      + {post.title} +
      + )} +
      +

      {post.title}

      + +
      + {post.content} +

      Stack

      +
      + {post.stack.map(tech => ( + + {tech} + + ))} +
      +
      +
      + ); +} diff --git a/app/[lang]/works/layout.tsx b/app/[lang]/works/layout.tsx new file mode 100644 index 0000000..d89017a --- /dev/null +++ b/app/[lang]/works/layout.tsx @@ -0,0 +1,34 @@ +import { calSans, inter } from '@/app/fonts'; +import { i18n } from '@/i18n-config'; + +export async function generateStaticParams() { + return i18n.locales.map(locale => ({ lang: locale })); +} + +export default async function WorksLayout( + props: { + children: React.ReactNode; + params: Promise<{ lang: string }>; + } +) { + const params = await props.params; + + const { + children + } = props; + + return ( + + +
      + {children} +
      + + + ); +} diff --git a/app/[lang]/works/page.tsx b/app/[lang]/works/page.tsx new file mode 100644 index 0000000..275c8b2 --- /dev/null +++ b/app/[lang]/works/page.tsx @@ -0,0 +1,69 @@ +import { getTranslation } from '@/get-translation'; +import { getWorkPosts } from '@/util/posts/work'; +import { Navigation } from '../components/navigation'; +import { Card } from '../components/card'; +import { Eye } from 'lucide-react'; +import Link from 'next/link'; + +export function generateStaticParams() { + return [{ lang: 'en' }, { lang: 'es' }]; +} + +export default async function WorkPage(props: { + params: Promise<{ lang: 'es' | 'en' }>; +}) { + const params = await props.params; + + const { lang } = params; + + const t = await getTranslation(lang); + const posts = await getWorkPosts(lang); + + return ( +
      + +
      +
      +

      + {t.works.title} +

      +

      {t.works.brief}

      +
      + +
      + +
      + {posts.map(post => ( + + +
      +
      + + {' '} + +
      + +

      + {post.title} +

      +

      + {post.brief} +

      +
      +

      + {t.projects.cta} +

      +
      +
      + +
      + ))} +
      +
      +
      +
      + ); +} diff --git a/bun.lockb b/bun.lockb index ed40ffe..d35c70d 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/constants/navigation.ts b/constants/navigation.ts index 3dac17e..28d0d1f 100644 --- a/constants/navigation.ts +++ b/constants/navigation.ts @@ -2,11 +2,13 @@ export const navigation = { es: [ { name: 'Inicio', href: '/' }, { name: 'Resumen', href: '/resume' }, + { name: 'Trabajos', href: '/works' }, { name: 'Proyectos', href: '/projects' }, ], en: [ { name: 'Home', href: '/' }, { name: 'Resume', href: '/resume' }, + { name: 'Works', href: '/works' }, { name: 'Projects', href: '/projects' }, ], }; diff --git a/content/work/en/comuny-t.md b/content/work/en/comuny-t.md new file mode 100644 index 0000000..cd53db8 --- /dev/null +++ b/content/work/en/comuny-t.md @@ -0,0 +1,19 @@ +--- +title: “Comuny-T” +brief: “Invest easily, quickly and securely with no minimum amounts in real state.” +date: "06-01-2023" +image: “https://res.cloudinary.com/digohrwkz/image/upload/v1731865012/comunyt-preview_wznyim.png” +url: “https://comunyt.com/” +stack : [“ReactJS”, “Chakra UI”] +--- + +Comuny-T is an investor community that allows you to invest in the real state market easily and with no minimum amounts through cryptoassets. + +## Functionalities + +- Website creation +- Creation of several sections of the backoffice application. + +## Resume +It was a layout work from a design. Consumption of internal APIs of the company to obtain some data to display both on the home page and in the backoffice. + diff --git a/content/work/en/mocka-software.md b/content/work/en/mocka-software.md new file mode 100644 index 0000000..1bd8296 --- /dev/null +++ b/content/work/en/mocka-software.md @@ -0,0 +1,18 @@ +--- +title: “Mocka Software” +brief: “We transform your ideas into what your customers need. Let's grow your business together.” +date: “08-12-2024” +image: “https://res.cloudinary.com/digohrwkz/image/upload/v1731865010/mocka-preview_okzc2i.png” +url: “https://mockasoftware.com/” +stack : [“ReactJS”, “NextJS”, “TailwindCSS”, “i18n”, “HonoJS”, “Bun”, “Brevo”] +--- + +Mocka is a software agency that offers different services related to building digital products such as websites, mobile applications or bots with A.I. They also offer other types of more technical services such as Web Hosting, SEO Optimization. + +## Features + +- Complete Landing page with several sections. +- Multiple language support. +- Serverless backend for sending emails. +- Web and backend deployment. + diff --git a/content/work/es/comuny-t.md b/content/work/es/comuny-t.md new file mode 100644 index 0000000..e78a7ee --- /dev/null +++ b/content/work/es/comuny-t.md @@ -0,0 +1,18 @@ +--- +title: "Comuny-T" +brief: "Invertí de forma fácil, rápida y segura sin montos mínimos en real state." +date: "01-06-2023" +image: "https://res.cloudinary.com/digohrwkz/image/upload/v1731865012/comunyt-preview_wznyim.png" +url: "https://comunyt.com/" +stack : ["ReactJS", "Chakra UI"] +--- + +Comuny-T es una comunidad de inversores que te permite invertir en el mercado de real state de forma fácil y sin montos mínimos a través de criptoactivos. + +## Funcionalidades + +- Creación del sitio web +- Creación de varias secciones de la aplicación de backoffice. + +## Resumen +Fue un trabajo de maquetado apartir de un diseño. Consumo de APIs internas de la empresa para la obtención de algunos datos a mostrar tanto en la página principal como en el backoffice. \ No newline at end of file diff --git a/content/work/es/mocka-software.md b/content/work/es/mocka-software.md new file mode 100644 index 0000000..8c49a96 --- /dev/null +++ b/content/work/es/mocka-software.md @@ -0,0 +1,17 @@ +--- +title: "Mocka Software" +brief: "Transformamos tus ideas en los que sus clientes necesitan. Hagamos crecer tu negocio juntos." +date: 12-08-2024” +image: "https://res.cloudinary.com/digohrwkz/image/upload/v1731865010/mocka-preview_okzc2i.png" +url: "https://mockasoftware.com/" +stack : ["ReactJS", "NextJS", "TailwindCSS", "i18n", "HonoJS", "Bun", "Brevo"] +--- + +Mocka es una agencia de software que ofrece distintos servicios relacionados con la construcción de productos digitales como sitios webs, aplicaciones móviles o bots con I.A. También ofrecen otros tipos de servicios más técnicos como Web Hosting, Optimización de SEO. + +## Funcionalidades + +- Landing page completa con varias secciones. +- Soporte de múltiples idiomas. +- Serverless backend para envío de emails. +- Despliegue de web y backend. diff --git a/next.config.js b/next.config.js index 767719f..b24ecca 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,10 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + images: { + remotePatterns: [{ + hostname: 'res.cloudinary.com' + }] + } +} module.exports = nextConfig diff --git a/package.json b/package.json index 1677cf8..2be1a63 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,13 @@ "autoprefixer": "10.4.14", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", + "date-fns": "^4.1.0", "eslint": "8.41.0", "eslint-config-next": "13.4.3", "framer-motion": "^10.12.16", + "gray-matter": "^4.0.3", "lucide-react": "^0.221.0", + "markdown-to-jsx": "^7.6.2", "negotiator": "^0.6.3", "next": "^14.2.4", "postcss": "8.4.23", diff --git a/public/images/works/comunyt-preview.png b/public/images/works/comunyt-preview.png new file mode 100644 index 0000000..f271513 Binary files /dev/null and b/public/images/works/comunyt-preview.png differ diff --git a/public/images/works/mocka-preview.png b/public/images/works/mocka-preview.png new file mode 100644 index 0000000..2ec2d67 Binary files /dev/null and b/public/images/works/mocka-preview.png differ diff --git a/translations/en.json b/translations/en.json index 8110aa1..b3fcbb0 100644 --- a/translations/en.json +++ b/translations/en.json @@ -5,6 +5,7 @@ "experience": "Experience", "projects": "Projects", "services": "Services", + "works": "Works", "stack": "Stack", "certifications": "Certifications" }, @@ -29,5 +30,9 @@ "title": "Projects", "brief": "Some of my most important or recent projects that I have been working on.", "cta": "Read more" + }, + "works": { + "title": "Works", + "brief": "Some of the most important works that I have been working on." } } diff --git a/translations/es.json b/translations/es.json index 7db1e39..d09bfa8 100644 --- a/translations/es.json +++ b/translations/es.json @@ -5,6 +5,7 @@ "experience": "Experiencia", "projects": "Proyectos", "services": "Servicios", + "works": "Trabajos", "stack": "Stack", "certifications": "Certificaciones" }, @@ -29,5 +30,9 @@ "title": "Proyectos", "brief": "Algunos de los proyectos más importantes que hice o en los que estuve trabajando.", "cta": "Leer más" + }, + "works": { + "title": "Trabajos", + "brief": "Algunos de los trabajos más importantes que hice o en los que estuve trabajando." } } diff --git a/util/posts/work.ts b/util/posts/work.ts new file mode 100644 index 0000000..80a148f --- /dev/null +++ b/util/posts/work.ts @@ -0,0 +1,61 @@ +import fs from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; + +const WORK_DIR = path.join(process.cwd(), 'content/work'); + +export interface WorkPost { + slug: string; + title: string; + brief: string + stack: string[] + url: string | undefined + date: string; + content: string; + image?: string; +} + +export async function getWorkPosts(lang: string): Promise { + const postsDirectory = path.join(WORK_DIR, lang); + const files = fs.readdirSync(postsDirectory); + + const posts = files + .filter((file) => file.endsWith('.md')) + .map((file) => { + const slug = file.replace(/\.md$/, ''); + const fullPath = path.join(postsDirectory, file); + const fileContents = fs.readFileSync(fullPath, 'utf8'); + const { data, content } = matter(fileContents); + + return { + slug, + title: data.title, + date: data.date, + brief: data.brief, + stack: data.stack, + url: data.url, + content, + image: data.image, + }; + }) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return posts; +} + +export async function getWorkPost(lang: string, slug: string): Promise { + const fullPath = path.join(WORK_DIR, lang, `${slug}.md`); + const fileContents = fs.readFileSync(fullPath, 'utf8'); + const { data, content } = matter(fileContents); + + return { + slug, + title: data.title, + brief: data.brief, + url: data.url, + date: data.date, + stack: data.stack, + content, + image: data.image, + }; +} \ No newline at end of file