Personalización del estilos con Tailwind CSS

¿Cuales son las ventajas de utilizar Tailwind CSS?

El framework CSS que utiliza t3-stack es TailwindCSS. Este permite:

  • Realizar actualizaciones en el diseño de forma relativamente sencilla.
  • No tener que escribir archivos CSS para cada uno de las páginas y/o componentes.
  • Tener un sistema de clases prefrabricadas para echar mano.
  • Arrancar el proyecto con un framework css en lugar de tener que empezar los estilos desde cero.

¿En conde puedo encontrar la paleta de colores de Tailwind CSS?

La paleta de colores de Tailwind CSS puede encontrarse aquí.

Dentro del archivo src/styles/globals.css vamos a agregar un estilo global que defina el fondo negro y el texto blanco.

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
    @apply bg-black text-slate-100;
}

Aplicando estilos a los componentes con TailwindCSS

Actualizamos el archivo del home src/pages/index.tsx con estilos de TailwindCSS.

import { SignIn, SignOutButton, useUser } from '@clerk/nextjs'
import Head from 'next/head'
import { api } from '~/utils/api'

export default function Home() {
    // ...

    const { data } = api.posts.getAll.useQuery()

    return (
        <>
            <Head>
                <title>Create T3 App</title>
                <meta name="description" content="Generated by create-t3-app" />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="flex h-screen justify-center">
                <div className="h-full w-full border-x border-slate-400 md:max-w-2xl">
                    <div className="flex border-b border-slate-400 p-4">
                        <div className="flex justify-center">
                            {isSignedIn && <SignOutButton />}
                        </div>
                        <div className="flex justify-center">
                            {!isSignedIn && <SignIn />}
                        </div>
                    </div>
                    <div className="flex flex-col">
                        {data?.map((post) => (
                            <div
                                key={`post-${post.id}`}
                                className="border-b border-slate-400 p-8"
                            >
                                {post.content}
                            </div>
                        ))}
                    </div>
                </div>
            </main>
        </>
    )
}

Y agregamos dos condicionales para cubrir escenarios donde no se pudieran retraer datos o se estén retrayendo estos.

import { SignIn, SignOutButton, useUser } from '@clerk/nextjs'
import Head from 'next/head'
import { api } from '~/utils/api'

export default function Home() {
    const { data, isLoading } = api.posts.getAll.useQuery()
    const { user, isSignedIn } = useUser()

    if (isLoading) {
        return <div>Loading...</div>
    }

    if (!data) {
        return <div>Something went wrong...</div>
    }

    return (
        <>
            <Head>
                <title>Create T3 App</title>
                <meta name="description" content="Generated by create-t3-app" />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="flex h-screen justify-center">
                <div className="h-full w-full border-x border-slate-400 md:max-w-2xl">
                    <div className="flex border-b border-slate-400 p-4">
                        <div className="flex justify-center">
                            {isSignedIn && <SignOutButton />}
                        </div>
                        <div className="flex justify-center">
                            {!isSignedIn && <SignIn />}
                        </div>
                    </div>
                    <div className="flex flex-col">
                        {data?.map((post) => (
                            <div
                                key={`post-${post.id}`}
                                className="border-b border-slate-400 p-8"
                            >
                                {post.content}
                            </div>
                        ))}
                    </div>
                </div>
            </main>
        </>
    )
}

Ahora vamos a complementar el home agregando un avatar de nuestro perfil seguido de un campo en donde podremos introducir texto.

import { SignIn, useUser } from '@clerk/nextjs'

const CreatePostWizard = () => {
    const { user, isSignedIn } = useUser()

    if (!user) {
        return null
    }

    return (
        <div className="flex w-full gap-3">
            <img
                src={imageUrl}
                alt="Profile Image"
                className="h-14 w-14 rounded-full"
            />
            <input
                placeholder="Type some emojis"
                className="grow bg-transparent outline-none"
            />
        </div>
    )
}

Obtener los autores de una publicación

Abrimos el archivo src/server/api/routers/post.ts y vamos a imprimir la lista de usuarios registrados en Clerk.

// getAll
getAll: publicProcedure.query(async ({ ctx }) => {
  // ...
  const users = await clerkClient.users.getUserList({});

  console.log(users);
  // ...

En la salida vamos a tomar los valores de id de diferentes usuarios para crear posts.

Abre prisma studio e inserta los datos necesarios.

pnpm prisma studio

Ahora vamos a actualizar el router para obtener ambos, posts y autores combinados en la respuesta de posts:getAll.

export const postsRouter = createTRPCRouter({
    getAll: publicProcedure.query(async ({ ctx }) => {
        // leer todos los posts
        const posts = await ctx.prisma.post.findMany({
            take: 100,
        })

        // si no hay posts retornar un arreglo vacío
        if (posts.length === 0) {
            return []
        }

        const authorsIds = posts.map((post) => post.authorId)

        const users = await clerkClient.users.getUserList({
            userId: authorsIds,
            limit: 100,
        })

        if (users.length === 0) {
            return []
        }

        const usersById = users.reduce((acc, user) => {
            acc[user.id] = user
            return acc
        }, {} as Record<string, (typeof users)[0]>)

        const updatedPosts = posts
            .filter((post) => post.authorId in usersById)
            .map((post) => ({
                post,
                author: usersById[post.authorId],
            }))

        return updatedPosts
    }),
})

Uso de la librería dayjs

Una de las posibilidades que ofrece la librería dayjs, es mostrar el tiempo que transcurrido desde la publicación.

Instalamos dayjs.

pnpm add -D dayjs

Importamos e inicializamos la librería de la siguiente forma.

import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)

Componente para mostrar los posts

PostView es el componente que nos permitirá ver la lista de posts.

// RouterOutputs permite importar tipos directamente de trpc
import type { RouterOutputs } from '~/utils/api'

// extraer el tipo directamente de trpc
type PostWithUser = RouterOutputs['posts']['getAll'][number]

// component para mostrar posts
const PostView = (props: PostWithUser) => {
    return (
        <div
            key={`post-${props.post.id}`}
            className="flex w-full gap-3 border-b border-slate-400 p-8"
        >
            <picture>
                <img
                    src={props.author?.imageUrl}
                    alt="Profile Image"
                    className="h-14 w-14 rounded-full"
                />
            </picture>
            <div>
                <div className="flex gap-3 font-bold">
                    @{props.author?.username} <span>·</span>{' '}
                    <span>{dayjs(props.post.createdAt).fromNow()}</span>
                </div>
                <div>{props.post.content}</div>
            </div>
        </div>
    )
}

Combinando las partes

Al final nos quedaría algo así.

import { SignIn, useUser } from '@clerk/nextjs'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import Head from 'next/head'
import type { RouterOutputs } from '~/utils/api'
import { api } from '~/utils/api'
dayjs.extend(relativeTime)

const CreatePostWizard = () => {
    const { user } = useUser()

    if (!user) {
        return null
    }

    return (
        <div className="flex w-full gap-3">
            <picture>
                <img
                    src={user.imageUrl}
                    alt="Profile Image"
                    className="h-14 w-14 rounded-full"
                />
            </picture>
            <input
                placeholder="Type some emojis"
                className="grow bg-transparent outline-none"
            />
        </div>
    )
}

type PostWithUser = RouterOutputs['posts']['getAll'][number]

const PostView = (props: PostWithUser) => {
    return (
        <div
            key={`post-${props.post.id}`}
            className="flex w-full gap-3 border-b border-slate-400 p-8"
        >
            <picture>
                <img
                    src={
                        props.author?.imageUrl ??
                        'https://dummyimage.com/800x600/ffffff/ffffff'
                    }
                    alt="Profile Image"
                    className="h-14 w-14 rounded-full"
                />
            </picture>
            <div>
                <div className="flex gap-3 font-bold">
                    @{props.author?.username} <span>·</span>{' '}
                    <span>{dayjs(props.post.createdAt).fromNow()}</span>
                </div>
                <div>{props.post.content}</div>
            </div>
        </div>
    )
}

export default function Home() {
    const { data, isLoading } = api.posts.getAll.useQuery()
    const { isSignedIn } = useUser()

    if (isLoading) {
        return <div>Loading...</div>
    }

    if (!data) {
        return <div>Something went wrong...</div>
    }

    return (
        <>
            <Head>
                <title>Create T3 App</title>
                <meta name="description" content="Generated by create-t3-app" />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="flex h-screen justify-center">
                <div className="h-full w-full border-x border-slate-400 md:max-w-2xl">
                    <div className="flex border-b border-slate-400 p-4">
                        <div className="flex w-full justify-center">
                            {isSignedIn && <CreatePostWizard />}
                        </div>
                        <div className="flex justify-center">
                            {!isSignedIn && <SignIn />}
                        </div>
                    </div>
                    <div className="flex flex-col">
                        {data?.map((fullPost) => (
                            <PostView
                                {...fullPost}
                                key={`post-view-${fullPost.post.id}`}
                            />
                        ))}
                    </div>
                </div>
            </main>
        </>
    )
}

Habilitar las rutas de trpc dentro del middleware

// src/middleware.ts
export default authMiddleware({
    publicRoutes: ['/', '/api/trpc/posts.getAll'],
})

Utilizando Tailwindcss hemos podido agregar estilos para personalizar la vista de nuestros posts dentro de nuestra aplicación. Talwindcss permite realizar esta personalización de manera sencilla, echando mano de una serie de clases listas para su uso.