Use Cases

Mengimplementasikan User Profile Features dengan Claude Code

Learn about implementing user profile features using Claude Code. Includes practical code examples.

penggunaprofil機能 dengan Claude Code: pembangunan

penggunaprofil SNS、SaaS、コミュニティサイト dll.多く aplikasi diperlukanな機能.Claude Code 使えば、アバターupload、profilEdit、publikasihalaman 含む完全な機能 efisien implementasi bisa dilakukan.

データモデル

> penggunaprofil機能 buatkan.
> アバターgambar upload、tampilan名・自己perkenalan・SNSリンク Edit、
> publikasiprofilhalaman implementasikan.
// 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;
}

profilpembaruanAPI

// 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: '認証 diperlukan す' }, { 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: '認証 diperlukan す' }, { 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: '認証 diperlukan す' }, { status: 401 });
  }

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

  if (!file) {
    return NextResponse.json({ error: 'file diperlukan す' }, { status: 400 });
  }

  // fileサイズチェック(5MB上限)
  if (file.size > 5 * 1024 * 1024) {
    return NextResponse.json({ error: 'fileサイズ 5MB以下 ください' }, { status: 400 });
  }

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

  // gambar リサイズ・optimasi
  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 });
}

profilEditform

// 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">表示名</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 ? 'penyimpanan中...' : 'penyimpanan'}
        </button>
      </div>
    </div>
  );
}

Artikel Terkait

gambarupload 詳細 fileuploadimplementasi、validasi 設計 formvalidasi juga bisa dijadikan referensi.

gambarpemrosesanlibrarysharp 公式dokumen(sharp.pixelplumbing.com) juga あわせてごkonfirmasi.

#Claude Code #profil #penggunamanajemen #gambarupload #React
Gratis

PDF Gratis: Cheatsheet Claude Code dalam 5 Menit

Cukup masukkan emailmu dan kami akan langsung mengirim cheatsheet PDF A4 satu halaman.

Kami menjaga data pribadimu dengan aman dan tidak pernah mengirim spam.

Masa

Tentang Penulis

Masa

Engineer yang aktif menggunakan Claude Code. Mengelola claudecode-lab.com, media teknologi 10 bahasa dengan lebih dari 2.000 halaman.