configure locales, fix bugs

This commit is contained in:
juancmandev 2024-07-31 16:42:52 -06:00
parent 373a4af4b1
commit 57cb8933e2
16 changed files with 173 additions and 103 deletions

View File

@ -8,7 +8,7 @@ import {
Info, Info,
Mail, Mail,
} from "lucide-react"; } from "lucide-react";
import { useTranslations, useTranslatedPath } from "@/i18n/utils"; import { useTranslations } from "@/i18n/utils";
type TNavItem = { type TNavItem = {
type: string; type: string;
@ -47,46 +47,22 @@ type Props = {
lang: "en" | "es"; lang: "en" | "es";
}; };
const locales = {
en: {
navigation: "Navigation",
blog: { label: "Blog", to: "/blog" },
portfolio: { label: "Portfolio", to: "/portfolio" },
videos: { label: "Videos", to: "/es/videos" },
microblog: { label: "Microblog", to: "/microblog" },
resources: { label: "Resources", to: "/resources" },
about: { label: "About", to: "/about" },
contact: { label: "Contact", to: "/contact" },
},
es: {
navigation: "Navegación",
blog: { label: "Blog", to: "/es/blog" },
portfolio: { label: "Portfolio", to: "/es/portfolio" },
videos: { label: "Videos", to: "/es/videos" },
microblog: { label: "Microblog", to: "/microblog" },
resources: { label: "Recursos", to: "/es/recursos" },
about: { label: "Acerca de", to: "/es/acerca-de" },
contact: { label: "Contacto", to: "/es/contacto" },
},
} as const;
export default function Navigation(props: Props) { export default function Navigation(props: Props) {
const t = useTranslations(props.lang); const t = useTranslations(props.lang as any);
const translatePath = useTranslatedPath(props.lang);
return ( return (
<nav className="px-4 sm:px-0 max-w-[65ch] mx-auto prose prose-invert pt-5 pb-20"> <nav className="px-4 sm:px-0 max-w-[65ch] mx-auto prose prose-invert pt-5 pb-20">
<h2 id="navigation">{locales[props.lang].navigation}</h2> <h2 id="navigation">{t("navigation")}</h2>
<ul className="list-none p-0 flex flex-wrap gap-4"> <ul className="list-none p-0 flex flex-wrap gap-4">
{navItems.map((navItem, index) => ( {navItems.map((navItem, index) => (
<li key={index} className="m-0 p-0"> <li key={index} className="m-0 p-0">
<LinkButton <LinkButton
variant="link" variant="link"
href={locales[props.lang][navItem.type].to} href={t(`${navItem.type}.to` as any)}
className="p-0 text-base gap-1" className="p-0 text-base gap-1"
> >
{navItem.icon} {navItem.icon}
{locales[props.lang][navItem.type].label} {t(`${navItem.type}.label` as any)}
</LinkButton> </LinkButton>
</li> </li>
))} ))}

View File

@ -0,0 +1,14 @@
---
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
---
Hola

View File

@ -1,12 +1,15 @@
--- ---
title: I will not continue creating content in Spanish for my website title: I will not continue creating content in Spanish for my website
description: I had the idea of maintaining my website in both English and Spanish; however, that is giving me some trouble, like taking more time to create content. description:
I had the idea of maintaining my website in both English and Spanish; however,
that is giving me some trouble, like taking more time to create content.
tags: [Personal] tags: [Personal]
image: /blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg image: /blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg
imageCaption: Letters mixed imageCaption: Letters mixed
date: 2024-2-20 date: 2024-2-20
author: Juan Manzanero author: Juan Manzanero
rss: true rss: true
draft: true
--- ---
![Letters mixed](@/assets/blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg) ![Letters mixed](@/assets/blog/i-will-not-continue-creating-content-in-spanish-for-my-website/banner.jpg)
@ -15,7 +18,11 @@ _Mixed letters. Photo by
on on
[Unsplash](https://unsplash.com/photos/red-alphabet-decors-0sBTrm726C8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_ [Unsplash](https://unsplash.com/photos/red-alphabet-decors-0sBTrm726C8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)_
I had the idea of maintaining my website in both English and Spanish; however, that is giving me some trouble, like taking more time to create content. _Note: I retaked redacting in Spanish for my website, but I'll keep this post
anyway_
I had the idea of maintaining my website in both English and Spanish; however,
that is giving me some trouble, like taking more time to create content.
That's why I have decided to only create content in English for now and in the That's why I have decided to only create content in English for now and in the
future. future.

View File

@ -1,23 +1,26 @@
--- ---
title: Contact title: Contact
description: You can contact me if you want me to work, or just say hello. description:
You can contact me if you want to say hi, contract me or ask me about a tool
that I use or some topic.
--- ---
# Contact # Contact
You can contact me if: You can contact me if:
- You want me to work - Just say hi
- Just say hello - You want to contract me
- Ask me about a tool that I use or some topic
Please consider that **I don't**: Please, consider that:
- Work for free - I won't work for free
- Work on your startup idea and just get equity in return (I can't pay my bills - I won't work on your startup idea, and I'll just get equity in return (I can't
with lottery tickets) pay my bills with lottery tickets)
- Work for you and get "exposure" (I can't pay my bills with exposure) - I won't work for you and get "exposure" (I can't pay my bills with exposure)
- Communicate via phone number; all communication must be via email (we can use - I won't communicate via phone number; all communication must be via email (we
Discord, Slack, etc. once you hire me). can use Discord, Slack, etc. once you hire me).
## My email ## My email

View File

@ -1,36 +1,40 @@
--- ---
title: Contact title: Contacto
description: You can contact me if you want me to work, or just say hello. description:
Puedes contactarme para decirme hola, contratarme o preguntarme sobre alguna
herramienta que uso o algún tema.
--- ---
# Contacto # Contacto
You can contact me if: Puedes contactarme si:
- You want me to work - Solo quieres decirme "hola"
- Just say hello - Quieres contratarme
- Preguntarme sobre alguna herramienta que utilizo o sobre algún tema
Please consider that **I don't**: Por favor, considera que:
- Work for free - No trabajo gratis
- Work on your startup idea and just get equity in return (I can't pay my bills - No trabajaré en tu idea de startup a cambio de equity (no puedo pagar mis
with lottery tickets) facturas con tickets de lotería)
- Work for you and get "exposure" (I can't pay my bills with exposure) - No trabajaré a cambio de "exposición" (no puedo pagar mis facturas con
- Communicate via phone number; all communication must be via email (we can use exposición)
Discord, Slack, etc. once you hire me). - No me comunicaré via número telefónico, toda comunicación debe ser via email
(podemos usar Discord, Slack, etc. una vez me contrates)
## My email ## Mi email
Just change `[at]` for `@` and `[dot]` for `.`. This is for preventing web Solo cambia `[at]` por `@` y `[dot]` por `.`. Esto es para evitar que web
crawlers from getting my email: crawlers obtengan mi email.
``` ```
contact[at]juancman[dot]dev contact[at]juancman[dot]dev
``` ```
## Social media ## Redes sociales
You can send me a direct message: Puedes enviarme un mensaje en:
- [LinkedIn](https://www.linkedin.com/in/juancmandev) - [LinkedIn](https://www.linkedin.com/in/juancmandev)
- [GitHub](https://github.com/juancmandev) - [GitHub](https://github.com/juancmandev)

View File

@ -0,0 +1,16 @@
---
title: Build a fullstack web app
description:
Build a fullstack web app using Next.js as meta-framework and PostgreSQL as
database.
tags: [Next.js, PostgreSQL, Prisma, Auth.js, tailwindcss, shadcn/ui]
image: /portfolio/build-a-fullstack-app/banner.png
imageCaption:
Banner with the tech stack used in this tutorial, Next.js, TailwindCSS,
shadcn/ui, Prisma, PostgreSQL and Auth.js.
date: 2024-1-18
author: Juan Manzanero
rss: true
---
Hola

View File

@ -1,16 +1,44 @@
export const languages = { export const languages = {
en: 'English', en: "English",
es: 'Español', es: "Español",
}; };
export const defaultLang = 'en'; export const defaultLang = "en";
export const showDefaultLang = false; export const showDefaultLang = false;
export const ui = { export const ui = {
en: { en: {
'hello': 'Hello', navigation: "Navigation",
}, "blog.label": "Blog",
es: { "blog.to": "/blog",
'hello': 'Hola', "portfolio.label": "Portfolio",
}, "portfolio.to": "/portfolio",
} as const; "videos.label": "Videos",
"videos.to": "/es/videos",
"microblog.label": "Microblog",
"microblog.to": "/microblog",
"resources.label": "Resources",
"resources.to": "/resources",
"about.label": "About",
"about.to": "/about",
"contact.label": "Contact",
"contact.to": "/contact",
},
es: {
navigation: "Navegación",
"blog.label": "Blog",
"blog.to": "/es/blog",
"portfolio.label": "Portfolio",
"portfolio.to": "/es/portfolio",
"videos.label": "Videos",
"videos.to": "/es/videos",
"microblog.label": "Microblog",
"microblog.to": "/microblog",
"resources.label": "Recursos",
"resources.to": "/es/recursos",
"about.label": "Acerca de",
"about.to": "/es/acerca-de",
"contact.label": "Contacto",
"contact.to": "/es/contacto",
},
} as const;

View File

@ -1,19 +1,19 @@
import { ui, defaultLang, showDefaultLang } from './ui'; import { ui, defaultLang, showDefaultLang } from "@/i18n/ui";
export function getLangFromUrl(url: URL) { export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split('/'); const [, lang] = url.pathname.split("/");
if (lang in ui) return lang as keyof typeof ui; if (lang in ui) return lang as keyof typeof ui;
return defaultLang; return defaultLang;
} }
export function useTranslations(lang: keyof typeof ui) { export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof typeof ui[typeof defaultLang]) { return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key]; return ui[lang][key] || ui[defaultLang][key];
} };
} }
export function useTranslatedPath(lang: keyof typeof ui) { export function useTranslatedPath(lang: keyof typeof ui) {
return function translatePath(path: string, l: string = lang) { return function translatePath(path: string, l: string = lang) {
return !showDefaultLang && l === defaultLang ? path : `/${l}${path}` return !showDefaultLang && l === defaultLang ? path : `/${l}${path}`;
} };
} }

View File

@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@ -1,8 +1,8 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import components from "@/components/mdx/wrapper"; import components from "@/components/mdx/wrapper";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
page: CollectionEntry<"pages">; page: CollectionEntry<"pages">;

View File

@ -1,9 +1,9 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import components from "@/components/mdx/wrapper"; import components from "@/components/mdx/wrapper";
import formatDate from "@/utils/format-date"; import formatDate from "@/utils/format-date";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
post: CollectionEntry<"blog">; post: CollectionEntry<"blog">;

View File

@ -1,8 +1,8 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import components from "@/components/mdx/wrapper"; import components from "@/components/mdx/wrapper";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
page: CollectionEntry<"pages">; page: CollectionEntry<"pages">;

View File

@ -10,10 +10,10 @@ import type { ImageMetadata } from "astro";
const markdownParser = new MarkdownIt(); const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>( const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}", "/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}"
); );
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>( const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}", "/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}"
); );
export async function GET(context: any) { export async function GET(context: any) {
@ -21,14 +21,25 @@ export async function GET(context: any) {
const blog = await getCollection( const blog = await getCollection(
"blog", "blog",
({ data }) => data.draft !== true && data.rss === true, ({ data }) => data.draft !== true && data.rss === true
); );
const filterBlog = blog.filter((post) => {
const [lang] = post.slug.split("/");
return lang === "es" && post;
});
const portfolio = await getCollection( const portfolio = await getCollection(
"portfolio", "portfolio",
({ data }) => data.draft !== true && data.rss === true, ({ data }) => data.draft !== true && data.rss === true
); );
const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.slug.split("/");
for await (const post of blog) { return lang === "es" && project;
});
for await (const post of filterBlog) {
const body = markdownParser.render(post.body); const body = markdownParser.render(post.body);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll("img");
@ -40,14 +51,14 @@ export async function GET(context: any) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace("@/", "");
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then( const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
(res: any) => res.default, (res: any) => res.default
); );
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", "src",
context.site + optimizedImg.src.replace("/", ""), context.site + optimizedImg.src.replace("/", "")
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith("/images")) {
@ -68,7 +79,7 @@ export async function GET(context: any) {
}); });
} }
for await (const project of portfolio) { for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body); const body = markdownParser.render(project.body);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll("img");
@ -80,14 +91,14 @@ export async function GET(context: any) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace("@/", "");
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then( const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default, (res: any) => res.default
); );
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", "src",
context.site + optimizedImg.src.replace("/", ""), context.site + optimizedImg.src.replace("/", "")
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith("/images")) {

View File

@ -1,9 +1,9 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import components from "@/components/mdx/wrapper"; import components from "@/components/mdx/wrapper";
import formatDate from "@/utils/format-date"; import formatDate from "@/utils/format-date";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
project: CollectionEntry<"videos">; project: CollectionEntry<"videos">;

View File

@ -10,10 +10,10 @@ import type { ImageMetadata } from "astro";
const markdownParser = new MarkdownIt(); const markdownParser = new MarkdownIt();
const imagesBlog = import.meta.glob<{ default: ImageMetadata }>( const imagesBlog = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}", "/src/assets/blog/**/**/*.{jpeg,jpg,png,gif,webp}"
); );
const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>( const imagesPortfolio = import.meta.glob<{ default: ImageMetadata }>(
"/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}", "/src/assets/portfolio/**/**/*.{jpeg,jpg,png,gif,webp}"
); );
export async function GET(context: any) { export async function GET(context: any) {
@ -21,14 +21,25 @@ export async function GET(context: any) {
const blog = await getCollection( const blog = await getCollection(
"blog", "blog",
({ data }) => data.draft !== true && data.rss === true, ({ data }) => data.draft !== true && data.rss === true
); );
const filterBlog = blog.filter((post) => {
const [lang] = post.slug.split("/");
return lang !== "es" && post;
});
const portfolio = await getCollection( const portfolio = await getCollection(
"portfolio", "portfolio",
({ data }) => data.draft !== true && data.rss === true, ({ data }) => data.draft !== true && data.rss === true
); );
const filterPortfolio = portfolio.filter((project) => {
const [lang] = project.slug.split("/");
for await (const post of blog) { return lang !== "es" && project;
});
for await (const post of filterBlog) {
const body = markdownParser.render(post.body); const body = markdownParser.render(post.body);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll("img");
@ -40,14 +51,14 @@ export async function GET(context: any) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace("@/", "");
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesBlog[imagePathPrefix]?.()?.then( const imagePath = await imagesBlog[imagePathPrefix]?.()?.then(
(res: any) => res.default, (res: any) => res.default
); );
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", "src",
context.site + optimizedImg.src.replace("/", ""), context.site + optimizedImg.src.replace("/", "")
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith("/images")) {
@ -68,7 +79,7 @@ export async function GET(context: any) {
}); });
} }
for await (const project of portfolio) { for await (const project of filterPortfolio) {
const body = markdownParser.render(project.body); const body = markdownParser.render(project.body);
const html = htmlParser.parse(body); const html = htmlParser.parse(body);
const images = html.querySelectorAll("img"); const images = html.querySelectorAll("img");
@ -80,14 +91,14 @@ export async function GET(context: any) {
const prefixRemoved = src.replace("@/", ""); const prefixRemoved = src.replace("@/", "");
const imagePathPrefix = `/src/${prefixRemoved}`; const imagePathPrefix = `/src/${prefixRemoved}`;
const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then( const imagePath = await imagesPortfolio[imagePathPrefix]?.()?.then(
(res: any) => res.default, (res: any) => res.default
); );
if (imagePath) { if (imagePath) {
const optimizedImg = await getImage({ src: imagePath }); const optimizedImg = await getImage({ src: imagePath });
img.setAttribute( img.setAttribute(
"src", "src",
context.site + optimizedImg.src.replace("/", ""), context.site + optimizedImg.src.replace("/", "")
); );
} }
} else if (src.startsWith("/images")) { } else if (src.startsWith("/images")) {

View File

@ -1,9 +1,9 @@
--- ---
import Layout from "@/layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import components from "@/components/mdx/wrapper"; import components from "@/components/mdx/wrapper";
import formatDate from "@/utils/format-date"; import formatDate from "@/utils/format-date";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
project: CollectionEntry<"portfolio">; project: CollectionEntry<"portfolio">;