Use Cases

Claude Code के साथ Implementing User Profile Features

Claude Code का उपयोग करके implementing user profile features सीखें। Practical code examples शामिल हैं।

userプロフィールfeaturesको Claude Code सेbuildする

userプロフィールはSNS、SaaS、コミュニティサイト आदि多くのアプリにज़रूरीなfeatures है।Claude Code का उपयोग करके、アバターupload、プロフィールEdit、公開pageを含む完全なfeaturesをefficientlyimplementationでき है।

dataモデル

> userプロフィールfeaturesを作って。
> アバター画像のupload、display名・自己紹介・SNSlinkのEdit、
> 公開プロフィールpageをimplement करो。
// src/types/profile.ts
export interface UserProfile {
  id: string;
  userId: string;
  displayName: string;
  bio: string;
  avatarUrl?: string;
  location?: string;
  website?: string;
  socialLinks: {
    twitter?: string;
    github?: string;
    linkedin?: string;
  };
  isPublic: boolean;
  createdAt: Date;
  updatedAt: Date;
}

プロフィールupdateAPI

// src/app/api/profile/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';

const profileSchema = z.object({
  displayName: z.string().min(1).max(50),
  bio: z.string().max(500).optional(),
  location: z.string().max(100).optional(),
  website: z.string().url().optional().or(z.literal('')),
  socialLinks: z.object({
    twitter: z.string().optional(),
    github: z.string().optional(),
    linkedin: z.string().optional(),
  }).optional(),
  isPublic: z.boolean().optional(),
});

export async function PUT(request: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'authenticationがज़रूरीです' }, { status: 401 });
  }

  const body = await request.json();
  const validated = profileSchema.parse(body);

  const profile = await prisma.profile.upsert({
    where: { userId: session.user.id },
    update: { ...validated, updatedAt: new Date() },
    create: { ...validated, userId: session.user.id },
  });

  return NextResponse.json(profile);
}

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'authenticationがज़रूरीです' }, { status: 401 });
  }

  const profile = await prisma.profile.findUnique({
    where: { userId: session.user.id },
  });

  return NextResponse.json(profile);
}

アバターupload

// src/app/api/profile/avatar/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { uploadToS3 } from '@/lib/storage';
import sharp from 'sharp';

export async function POST(request: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ error: 'authenticationがज़रूरीです' }, { status: 401 });
  }

  const formData = await request.formData();
  const file = formData.get('avatar') as File;

  if (!file) {
    return NextResponse.json({ error: 'fileがज़रूरीです' }, { status: 400 });
  }

  // filesizecheck(5MBऊपर限)
  if (file.size > 5 * 1024 * 1024) {
    return NextResponse.json({ error: 'filesizeは5MBनिम्नलिखितにしてください' }, { status: 400 });
  }

  const buffer = Buffer.from(await file.arrayBuffer());

  // 画像をリsize・optimization
  const optimized = await sharp(buffer)
    .resize(256, 256, { fit: 'cover' })
    .webp({ quality: 80 })
    .toBuffer();

  const key = `avatars/${session.user.id}.webp`;
  const url = await uploadToS3(optimized, key, 'image/webp');

  await prisma.profile.update({
    where: { userId: session.user.id },
    data: { avatarUrl: url },
  });

  return NextResponse.json({ avatarUrl: url });
}

プロフィールEditform

// src/components/ProfileForm.tsx
'use client';
import { useState, useRef } from 'react';
import { UserProfile } from '@/types/profile';

export function ProfileForm({ profile }: { profile: UserProfile }) {
  const [form, setForm] = useState(profile);
  const [saving, setSaving] = useState(false);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('avatar', file);

    const res = await fetch('/api/profile/avatar', { method: 'POST', body: formData });
    const data = await res.json();
    setForm((prev) => ({ ...prev, avatarUrl: data.avatarUrl }));
  };

  const handleSave = async () => {
    setSaving(true);
    await fetch('/api/profile', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form),
    });
    setSaving(false);
  };

  return (
    <div className="max-w-2xl mx-auto space-y-6">
      <div className="flex items-center gap-6">
        <div
          onClick={() => fileInputRef.current?.click()}
          className="w-24 h-24 rounded-full bg-gray-200 overflow-hidden cursor-pointer hover:opacity-80"
        >
          {form.avatarUrl ? (
            <img src={form.avatarUrl} alt="アバター" className="w-full h-full object-cover" />
          ) : (
            <div className="w-full h-full flex items-center justify-center text-gray-400 text-3xl">+</div>
          )}
        </div>
        <input ref={fileInputRef} type="file" accept="image/*" onChange={handleAvatarChange} className="hidden" />
        <div>
          <p className="font-medium dark:text-white">プロフィール画像</p>
          <p className="text-sm text-gray-500">JPG, PNG, WebP(最大5MB)</p>
        </div>
      </div>

      <div className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-1 dark:text-gray-300">display名</label>
          <input
            value={form.displayName}
            onChange={(e) => setForm({ ...form, displayName: e.target.value })}
            className="w-full border rounded-lg px-4 py-2 dark:bg-gray-800 dark:border-gray-700"
          />
        </div>
        <div>
          <label className="block text-sm font-medium mb-1 dark:text-gray-300">自己紹介</label>
          <textarea
            value={form.bio}
            onChange={(e) => setForm({ ...form, bio: e.target.value })}
            className="w-full border rounded-lg px-4 py-2 h-24 dark:bg-gray-800 dark:border-gray-700"
            maxLength={500}
          />
          <p className="text-xs text-gray-400 mt-1">{form.bio.length}/500</p>
        </div>
        <button onClick={handleSave} disabled={saving} className="bg-blue-600 text-white px-6 py-2 rounded-lg disabled:opacity-50">
          {saving ? '保存में...' : '保存'}
        </button>
      </div>
    </div>
  );
}

関連記事

画像uploadके details के लिएfileuploadimplementation、validationの設計はformvalidationभी reference के लिए देखें。

画像processinglibrarysharpのofficial documentation(sharp.pixelplumbing.com)もあわせてごconfirm करें।

#Claude Code #プロフィール #usermanagement #画像upload #React