agentsclimarketplace

Senior frontend

Skill roedyrustam/claudevibeskills/src/senior-frontend

Koleksi 20 Claude Skills siap pakai untuk pengembangan SaaS, web modern, dan praktik rekayasa perangkat lunak tingkat lanjut.

Install
npx -y skills add roedyrustam/claudevibeskills --skill senior-frontend

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Expert frontend engineering skill for React 19, Next.js 15, and Tailwind CSS v4. Use this skill whenever the user is building, debugging, or reviewing frontend code — React components, server components, client components, hooks, forms, routing, layouts, data fetching, bundle optimization, or anything related to the modern React/Next.js ecosystem. Trigger on any mention of React, Next.js, Tailwind, useActionState, useOptimistic, useFormStatus, Server Components (RSC), Turbopack, App Router, or frontend performance. Also trigger for questions about component architecture, hydration errors, code-splitting, or image/font optimization.

SKILL.md

12.5 KB, as published. Nobody here has run it

Senior Frontend Engineer Skill

Expert guidance for modern React 19 + Next.js 15 + Tailwind CSS v4 development.


Core Stack

LayerTechnology
FrameworkNext.js 15 (App Router)
UI LibraryReact 19
StylingTailwind CSS v4
LanguageTypeScript 5.x (strict)
BundlerTurbopack (dev), webpack (prod fallback)
Package Managerpnpm or Bun

React 19 Patterns

New Hooks — Use These, Not the Old Equivalents

useActionState (replaces useFormState)

'use client'
import { useActionState } from 'react'
import { submitForm } from '@/app/actions'

type State = { error?: string; success?: boolean }

export function ContactForm() {
  const [state, formAction, isPending] = useActionState<State, FormData>(
    submitForm,
    { error: undefined, success: false }
  )

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      {state.error && <p className="text-red-500 text-sm">{state.error}</p>}
      <SubmitButton />
    </form>
  )
}

useOptimistic — Instant UI feedback

'use client'
import { useOptimistic, useTransition } from 'react'

type Todo = { id: string; text: string; done: boolean }

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  )
  const [isPending, startTransition] = useTransition()

  const handleAdd = (text: string) => {
    const tempTodo = { id: crypto.randomUUID(), text, done: false }
    startTransition(async () => {
      addOptimistic(tempTodo)
      await createTodo(text)
    })
  }

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} className={todo.done ? 'line-through opacity-50' : ''}>
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

useFormStatus — Submit button state

'use client'
import { useFormStatus } from 'react-dom'

export function SubmitButton({ label = 'Submit' }: { label?: string }) {
  const { pending } = useFormStatus()
  return (
    <button
      type="submit"
      disabled={pending}
      className="btn-primary disabled:opacity-60 disabled:cursor-not-allowed"
    >
      {pending ? 'Loading…' : label}
    </button>
  )
}

React 19 use() Hook

import { use, Suspense } from 'react'

// Pass a Promise directly to use()
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise) // suspends until resolved
  return <div>{user.name}</div>
}

// Usage: wrap in Suspense
export default function Page() {
  const userPromise = fetchUser(123)
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

Next.js 15 App Router Patterns

File Structure

app/
├── layout.tsx          # Root layout (server component)
├── page.tsx            # Home page
├── globals.css         # Tailwind CSS v4 entry
├── (marketing)/        # Route group (no URL segment)
│   ├── layout.tsx
│   └── about/page.tsx
├── (app)/              # Protected route group
│   ├── layout.tsx      # Auth guard here
│   └── dashboard/
│       ├── page.tsx
│       └── loading.tsx # Automatic Suspense boundary
├── api/
│   └── [...route]/
│       └── route.ts    # Route handlers
└── _components/        # Private folder (not routable)

Server vs Client Components

Rule: Default to Server Components. Add 'use client' only when needed.

NeedUse
Data fetching, DB access, secretsServer Component
useState, useEffect, event handlersClient Component
Browser APIs (window, localStorage)Client Component
Animations, gesture handlingClient Component
Everything elseServer Component
// ✅ Server Component — no directive needed
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } })
  if (!product) notFound()

  return (
    <div>
      <h1>{product.name}</h1>
      {/* Pass serializable data to client */}
      <AddToCartButton productId={product.id} />
    </div>
  )
}

Data Fetching in Server Components

// Next.js 15: fetch() is no longer cached by default!
// Must explicitly opt-in to caching.

// No cache (default in Next.js 15)
const data = await fetch('https://api.example.com/data')

// Cache permanently (like old Next.js default)
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache'
})

// Revalidate every N seconds
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// Tag-based revalidation
const data = await fetch('https://api.example.com/data', {
  next: { tags: ['products'] }
})
// Then call: revalidateTag('products') in a Server Action

Server Actions

// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const schema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
})

export async function createPost(prevState: unknown, formData: FormData) {
  const parsed = schema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  })

  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }

  try {
    await db.post.create({ data: parsed.data })
    revalidateTag('posts')
    revalidatePath('/blog')
  } catch {
    return { error: { _form: ['Database error. Please try again.'] } }
  }

  redirect('/blog') // Throws internally — don't wrap in try/catch
}

Parallel Routes & Intercepting Routes

app/
├── @modal/           # Parallel slot
│   └── (.)photo/[id]/
│       └── page.tsx  # Intercepted route (modal)
├── layout.tsx        # Receives { children, modal }
└── photo/[id]/
    └── page.tsx      # Full page (on hard refresh)
// app/layout.tsx
export default function Layout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

Tailwind CSS v4

Setup — CSS-First Configuration

/* app/globals.css */
@import "tailwindcss";

/* Define theme in CSS, not tailwind.config.js */
@theme {
  --color-brand-50: oklch(0.97 0.02 260);
  --color-brand-500: oklch(0.55 0.22 260);
  --color-brand-900: oklch(0.25 0.12 260);

  --font-sans: "Inter Variable", sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  --radius-card: 0.75rem;
  --shadow-card: 0 4px 24px oklch(0 0 0 / 0.08);

  --animate-fade-in: fade-in 0.2s ease-out;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

v4 Breaking Changes to Know

// ❌ v3 — JIT config in tailwind.config.js
// ✅ v4 — everything in CSS via @theme

// ❌ v3
<div className="bg-opacity-50">
// ✅ v4 — use slash syntax
<div className="bg-black/50">

// ❌ v3 — ring-offset
<div className="ring-2 ring-offset-2">
// ✅ v4 — outline or shadow instead

// New: arbitrary values with CSS vars
<div className="text-[var(--color-brand-500)]">

Component Patterns with Tailwind v4

// Use cva for variant-based components
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-brand-500 text-white hover:bg-brand-600',
        outline: 'border border-brand-500 text-brand-500 hover:bg-brand-50',
        ghost: 'hover:bg-brand-50 text-brand-500',
        destructive: 'bg-red-500 text-white hover:bg-red-600',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: { variant: 'default', size: 'md' },
  }
)

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
  )
}

Performance Optimization

Image Optimization

import Image from 'next/image'

// Always specify width/height or fill
<Image
  src="/hero.webp"
  alt="Hero image"
  width={1200}
  height={630}
  priority            // LCP image — load eagerly
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
/>

// Dynamic images — use fill + sized container
<div className="relative aspect-video w-full">
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    className="object-cover rounded-lg"
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  />
</div>

Bundle Optimization

// Dynamic imports for heavy components
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Client-only components
})

const RichEditor = dynamic(() => import('@/components/RichEditor'), {
  loading: () => <EditorSkeleton />,
})

Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',  // Use as CSS variable
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.variable}>
      <body className="font-sans">{children}</body>
    </html>
  )
}

Metadata & SEO

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.ogImage, width: 1200, height: 630 }],
    },
    alternates: {
      canonical: `https://example.com/blog/${params.slug}`,
    },
  }
}

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

Error Handling Patterns

// error.tsx — must be 'use client'
'use client'
import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    console.error(error)
    // Log to Sentry, Datadog, etc.
  }, [error])

  return (
    <div className="flex flex-col items-center gap-4 p-8">
      <h2 className="text-xl font-semibold">Something went wrong</h2>
      <p className="text-muted-foreground text-sm">{error.message}</p>
      <button onClick={reset} className="btn-primary">Try again</button>
    </div>
  )
}

Key Rules (Always Follow)

  1. Never use any — use unknown and narrow types, or proper generics
  2. Server Components by default — only 'use client' when necessary
  3. Validate all Server Action inputs with Zod before touching DB
  4. Never redirect() inside try/catch — it throws internally
  5. Use next/image for all <img> tags
  6. Use next/font — never <link> to Google Fonts directly
  7. Suspense boundaries around async data fetching trees
  8. loading.tsx for route-level skeletons (automatic Suspense)
  9. error.tsx for route-level error recovery (must be 'use client')
  10. not-found.tsx for 404 states — call notFound() from server components

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.