← back to kenta-yos__auto-sns

Function bodies 68 total

All specs Real LLM only Function bodies
seed function · typescript · L6-L24 (19 LOC)
scripts/seed.ts
async function seed() {
  const url = process.env.DATABASE_URL;
  if (!url) {
    console.error("DATABASE_URL is not set");
    process.exit(1);
  }

  const sql = neon(url);
  const db = drizzle(sql);

  const email = process.argv[2] || "[email protected]";
  const password = process.argv[3] || "admin123";

  const passwordHash = await hash(password, 12);

  await db.insert(users).values({ email, passwordHash }).onConflictDoNothing();

  console.log(`User seeded: ${email}`);
}
GET function · typescript · L12-L115 (104 LOC)
src/app/api/analytics/bluesky/route.ts
export async function GET(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { searchParams } = new URL(req.url);
  const days = parseInt(searchParams.get("days") || "30", 10);

  const since = new Date();
  since.setDate(since.getDate() - days);

  // 各投稿の最新メトリクス
  const postMetrics = await db
    .select()
    .from(blueskyPostMetrics)
    .where(gte(blueskyPostMetrics.collectedAt, since))
    .orderBy(desc(blueskyPostMetrics.collectedAt));

  // 投稿URIごとに最新のスナップショットだけ取得
  const latestByUri = new Map<
    string,
    {
      platformPostUri: string;
      likeCount: number;
      repostCount: number;
      replyCount: number;
      collectedAt: Date;
      postId: string;
    }
  >();

  for (const m of postMetrics) {
    if (!latestByUri.has(m.platformPostUri)) {
      latestByUri.set(m.platformPostUri, m);
    }
  }

  // 投稿情報を紐付け
  const enriched = await Promise.all(
    Array.from(latestByUri.values()).map(async (m)
POST function · typescript · L13-L56 (44 LOC)
src/app/api/auth/login/route.ts
export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const parsed = loginSchema.safeParse(body);
    if (!parsed.success) {
      return NextResponse.json(
        { error: "メールアドレスとパスワードを入力してください" },
        { status: 400 }
      );
    }

    const { email, password } = parsed.data;
    const [user] = await db
      .select()
      .from(users)
      .where(eq(users.email, email))
      .limit(1);

    if (!user) {
      return NextResponse.json(
        { error: "認証情報が正しくありません" },
        { status: 401 }
      );
    }

    const valid = await verifyPassword(password, user.passwordHash);
    if (!valid) {
      return NextResponse.json(
        { error: "認証情報が正しくありません" },
        { status: 401 }
      );
    }

    const token = await createToken(user.id);
    await setAuthCookie(token);

    return NextResponse.json({ success: true });
  } catch {
    return NextResponse.json(
      { error: "サーバーエラーが発生しました" },
      { status: 500 }
    );
 
POST function · typescript · L4-L7 (4 LOC)
src/app/api/auth/logout/route.ts
export async function POST() {
  await removeAuthCookie();
  return NextResponse.json({ success: true });
}
GET function · typescript · L19-L101 (83 LOC)
src/app/api/cron/collect-metrics/route.ts
export async function GET(req: NextRequest) {
  const authError = verifyCronSecret(req);
  if (authError) return authError;

  // Bluesky の認証情報を取得(最初に見つかったもの)
  const [cred] = await db
    .select()
    .from(platformCredentials)
    .where(eq(platformCredentials.platform, "bluesky"))
    .limit(1);

  if (!cred) {
    return NextResponse.json(
      { error: "Bluesky credentials not configured" },
      { status: 400 }
    );
  }

  const credentials = JSON.parse(
    decrypt(cred.encrypted as { iv: string; authTag: string; ciphertext: string })
  ) as BlueskyCredentials;

  const collected = { postMetrics: 0, profileMetrics: 0, errors: [] as string[] };

  // 1. 過去30日の Bluesky 投稿のメトリクス収集
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  const blueskyResults = await db
    .select()
    .from(postPlatformResults)
    .where(
      and(
        eq(postPlatformResults.platform, "bluesky"),
        eq(postPlatformResults.success, 1),
        gt
GET function · typescript · L8-L45 (38 LOC)
src/app/api/cron/process-scheduled/route.ts
export async function GET(req: NextRequest) {
  const authError = verifyCronSecret(req);
  if (authError) return authError;

  const now = new Date();

  // scheduled_at <= now() かつ status = "scheduled" の投稿を取得
  const scheduledPosts = await db
    .select()
    .from(posts)
    .where(
      and(
        eq(posts.status, "scheduled"),
        lte(posts.scheduledAt, now)
      )
    );

  const results = [];

  for (const post of scheduledPosts) {
    try {
      const result = await publishPost(post.id);
      results.push({ postId: post.id, success: true, result });
    } catch (e) {
      results.push({
        postId: post.id,
        success: false,
        error: e instanceof Error ? e.message : "Unknown error",
      });
    }
  }

  return NextResponse.json({
    processed: scheduledPosts.length,
    results,
    processedAt: now.toISOString(),
  });
}
GET function · typescript · L16-L35 (20 LOC)
src/app/api/posts/[id]/route.ts
export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { id } = await params;
  const [post] = await db
    .select()
    .from(posts)
    .where(and(eq(posts.id, id), eq(posts.userId, auth.userId)))
    .limit(1);

  if (!post) {
    return NextResponse.json({ error: "投稿が見つかりません" }, { status: 404 });
  }

  return NextResponse.json(post);
}
If a scraper extracted this row, it came from Repobility (https://repobility.com)
PATCH function · typescript · L38-L86 (49 LOC)
src/app/api/posts/[id]/route.ts
export async function PATCH(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { id } = await params;
  const body = await req.json();
  const parsed = updateSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: "不正なリクエスト" }, { status: 400 });
  }

  const [existing] = await db
    .select()
    .from(posts)
    .where(and(eq(posts.id, id), eq(posts.userId, auth.userId)))
    .limit(1);

  if (!existing) {
    return NextResponse.json({ error: "投稿が見つかりません" }, { status: 404 });
  }

  if (existing.status !== "draft" && existing.status !== "scheduled") {
    return NextResponse.json(
      { error: "この投稿は編集できません" },
      { status: 400 }
    );
  }

  const updates: Record<string, unknown> = { updatedAt: new Date() };
  if (parsed.data.body !== undefined) updates.body = parsed.data.body;
  if (parsed.data.platforms !== undefined)
    up
DELETE function · typescript · L89-L102 (14 LOC)
src/app/api/posts/[id]/route.ts
export async function DELETE(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { id } = await params;
  await db
    .delete(posts)
    .where(and(eq(posts.id, id), eq(posts.userId, auth.userId)));

  return NextResponse.json({ success: true });
}
POST function · typescript · L9-L38 (30 LOC)
src/app/api/posts/publish/route.ts
export async function POST(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { postId } = await req.json();
  if (!postId) {
    return NextResponse.json({ error: "postId が必要です" }, { status: 400 });
  }

  // 投稿存在確認 + 所有者チェック
  const [post] = await db
    .select()
    .from(posts)
    .where(and(eq(posts.id, postId), eq(posts.userId, auth.userId)))
    .limit(1);

  if (!post) {
    return NextResponse.json({ error: "投稿が見つかりません" }, { status: 404 });
  }

  if (post.status === "published" || post.status === "publishing") {
    return NextResponse.json(
      { error: "この投稿はすでに投稿済みまたは投稿中です" },
      { status: 400 }
    );
  }

  const results = await publishPost(postId);
  return NextResponse.json({ results });
}
GET function · typescript · L15-L37 (23 LOC)
src/app/api/posts/route.ts
export async function GET(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const allPosts = await db
    .select()
    .from(posts)
    .where(eq(posts.userId, auth.userId))
    .orderBy(desc(posts.createdAt));

  // 各投稿のプラットフォーム結果を取得
  const postsWithResults = await Promise.all(
    allPosts.map(async (post) => {
      const results = await db
        .select()
        .from(postPlatformResults)
        .where(eq(postPlatformResults.postId, post.id));
      return { ...post, results };
    })
  );

  return NextResponse.json(postsWithResults);
}
POST function · typescript · L40-L69 (30 LOC)
src/app/api/posts/route.ts
export async function POST(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const body = await req.json();
  const parsed = createSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.issues[0].message },
      { status: 400 }
    );
  }

  const { body: postBody, platforms, scheduledAt } = parsed.data;

  const status = scheduledAt ? "scheduled" : "draft";

  const [post] = await db
    .insert(posts)
    .values({
      userId: auth.userId,
      body: postBody,
      platforms,
      status,
      scheduledAt: scheduledAt ? new Date(scheduledAt) : null,
    })
    .returning();

  return NextResponse.json(post, { status: 201 });
}
GET function · typescript · L25-L48 (24 LOC)
src/app/api/settings/credentials/route.ts
export async function GET(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const creds = await db
    .select({
      platform: platformCredentials.platform,
      updatedAt: platformCredentials.updatedAt,
    })
    .from(platformCredentials)
    .where(eq(platformCredentials.userId, auth.userId));

  // 各プラットフォームのマスク情報を返す
  const result: Record<string, { configured: boolean; updatedAt: Date | null }> = {
    x: { configured: false, updatedAt: null },
    bluesky: { configured: false, updatedAt: null },
  };

  for (const c of creds) {
    result[c.platform] = { configured: true, updatedAt: c.updatedAt };
  }

  return NextResponse.json(result);
}
POST function · typescript · L51-L90 (40 LOC)
src/app/api/settings/credentials/route.ts
export async function POST(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const body = await req.json();
  const parsed = saveSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: "不正なリクエスト" }, { status: 400 });
  }

  const { platform, credentials } = parsed.data;
  const encrypted = encrypt(JSON.stringify(credentials));

  // upsert
  const existing = await db
    .select()
    .from(platformCredentials)
    .where(
      and(
        eq(platformCredentials.userId, auth.userId),
        eq(platformCredentials.platform, platform)
      )
    )
    .limit(1);

  if (existing.length > 0) {
    await db
      .update(platformCredentials)
      .set({ encrypted, updatedAt: new Date() })
      .where(eq(platformCredentials.id, existing[0].id));
  } else {
    await db.insert(platformCredentials).values({
      userId: auth.userId,
      platform,
      encrypted,
    });
  }

  return NextRespo
PUT function · typescript · L93-L114 (22 LOC)
src/app/api/settings/credentials/route.ts
export async function PUT(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const body = await req.json();
  const parsed = testSchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json({ error: "不正なリクエスト" }, { status: 400 });
  }

  const { platform, credentials } = parsed.data;

  if (platform === "x") {
    const result = await testXConnection(credentials as unknown as XCredentials);
    return NextResponse.json(result);
  } else {
    const result = await testBlueskyConnection(
      credentials as unknown as BlueskyCredentials
    );
    return NextResponse.json(result);
  }
}
All rows above produced by Repobility · https://repobility.com
DELETE function · typescript · L117-L137 (21 LOC)
src/app/api/settings/credentials/route.ts
export async function DELETE(req: NextRequest) {
  const auth = await requireAuth(req);
  if (auth instanceof NextResponse) return auth;

  const { platform } = await req.json();
  if (!["x", "bluesky"].includes(platform)) {
    return NextResponse.json({ error: "不正なプラットフォーム" }, { status: 400 });
  }

  // 復号してユーザー名を取得したい場合のために残す
  await db
    .delete(platformCredentials)
    .where(
      and(
        eq(platformCredentials.userId, auth.userId),
        eq(platformCredentials.platform, platform)
      )
    );

  return NextResponse.json({ success: true });
}
ComposePage function · typescript · L4-L14 (11 LOC)
src/app/dashboard/compose/page.tsx
export default function ComposePage() {
  const allDays = getTemplatesByDate();
  const today = getTodayTemplates();

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">投稿作成</h1>
      <PostComposer allDays={allDays} todayDate={today?.date || null} />
    </div>
  );
}
DashboardLayout function · typescript · L13-L92 (80 LOC)
src/app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const pathname = usePathname();
  const router = useRouter();

  async function handleLogout() {
    await fetch("/api/auth/logout", { method: "POST" });
    router.push("/login");
  }

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Header */}
      <header className="bg-white border-b sticky top-0 z-10">
        <div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
          <Link href="/dashboard" className="font-bold text-lg">
            auto-sns
          </Link>
          <button
            onClick={handleLogout}
            className="text-sm text-gray-500 hover:text-gray-700"
          >
            ログアウト
          </button>
        </div>
      </header>

      <div className="max-w-6xl mx-auto px-4 py-6 flex gap-6">
        {/* Sidebar */}
        <nav className="w-48 shrink-0 hidden md:block">
          <ul className="space-y-1">
     
handleLogout function · typescript · L21-L24 (4 LOC)
src/app/dashboard/layout.tsx
  async function handleLogout() {
    await fetch("/api/auth/logout", { method: "POST" });
    router.push("/login");
  }
DashboardPage function · typescript · L33-L95 (63 LOC)
src/app/dashboard/page.tsx
export default function DashboardPage() {
  const [data, setData] = useState<AnalyticsData | null>(null);
  const [loading, setLoading] = useState(true);
  const [days, setDays] = useState(30);

  useEffect(() => {
    async function fetchData() {
      setLoading(true);
      const res = await fetch(`/api/analytics/bluesky?days=${days}`);
      if (res.ok) {
        const json = await res.json();
        setData(json);
      }
      setLoading(false);
    }
    fetchData();
  }, [days]);

  if (loading) return <div className="text-gray-500">読み込み中...</div>;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-bold">ダッシュボード</h1>
        <div className="flex items-center gap-3">
          <select
            value={days}
            onChange={(e) => setDays(Number(e.target.value))}
            className="px-3 py-2 border rounded-lg text-sm"
          >
            <option value={7}>過去7日</option>
       
fetchData function · typescript · L39-L47 (9 LOC)
src/app/dashboard/page.tsx
    async function fetchData() {
      setLoading(true);
      const res = await fetch(`/api/analytics/bluesky?days=${days}`);
      if (res.ok) {
        const json = await res.json();
        setData(json);
      }
      setLoading(false);
    }
PostsPage function · typescript · L3-L10 (8 LOC)
src/app/dashboard/posts/page.tsx
export default function PostsPage() {
  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">投稿履歴</h1>
      <PostList />
    </div>
  );
}
SettingsPage function · typescript · L10-L281 (272 LOC)
src/app/dashboard/settings/page.tsx
export default function SettingsPage() {
  const [statuses, setStatuses] = useState<Record<string, PlatformStatus>>({});
  const [loading, setLoading] = useState(true);

  // X credentials
  const [xApiKey, setXApiKey] = useState("");
  const [xApiSecret, setXApiSecret] = useState("");
  const [xAccessToken, setXAccessToken] = useState("");
  const [xAccessSecret, setXAccessSecret] = useState("");

  // Bluesky credentials
  const [bsIdentifier, setBsIdentifier] = useState("");
  const [bsPassword, setBsPassword] = useState("");

  const [message, setMessage] = useState("");
  const [testResult, setTestResult] = useState("");

  useEffect(() => {
    fetchStatuses();
  }, []);

  async function fetchStatuses() {
    const res = await fetch("/api/settings/credentials");
    const data = await res.json();
    setStatuses(data);
    setLoading(false);
  }

  async function saveCredentials(
    platform: string,
    credentials: Record<string, string>
  ) {
    setMessage("");
    const re
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
fetchStatuses function · typescript · L31-L36 (6 LOC)
src/app/dashboard/settings/page.tsx
  async function fetchStatuses() {
    const res = await fetch("/api/settings/credentials");
    const data = await res.json();
    setStatuses(data);
    setLoading(false);
  }
saveCredentials function · typescript · L38-L55 (18 LOC)
src/app/dashboard/settings/page.tsx
  async function saveCredentials(
    platform: string,
    credentials: Record<string, string>
  ) {
    setMessage("");
    const res = await fetch("/api/settings/credentials", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ platform, credentials }),
    });

    if (res.ok) {
      setMessage(`${platform} の認証情報を保存しました`);
      fetchStatuses();
    } else {
      setMessage("保存に失敗しました");
    }
  }
testConnection function · typescript · L57-L76 (20 LOC)
src/app/dashboard/settings/page.tsx
  async function testConnection(
    platform: string,
    credentials: Record<string, string>
  ) {
    setTestResult("");
    const res = await fetch("/api/settings/credentials", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ platform, credentials }),
    });

    const data = await res.json();
    if (data.success) {
      setTestResult(
        `接続成功: ${data.username || data.handle}`
      );
    } else {
      setTestResult(`接続失敗: ${data.error}`);
    }
  }
deleteCredentials function · typescript · L78-L89 (12 LOC)
src/app/dashboard/settings/page.tsx
  async function deleteCredentials(platform: string) {
    if (!confirm(`${platform} の認証情報を削除しますか?`)) return;
    const res = await fetch("/api/settings/credentials", {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ platform }),
    });
    if (res.ok) {
      setMessage(`${platform} の認証情報を削除しました`);
      fetchStatuses();
    }
  }
RootLayout function · typescript · L20-L34 (15 LOC)
src/app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
LoginPage function · typescript · L6-L90 (85 LOC)
src/app/login/page.tsx
export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError("");
    setLoading(true);

    try {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });
      const data = await res.json();

      if (!res.ok) {
        setError(data.error || "ログインに失敗しました");
        return;
      }

      router.push("/dashboard");
    } catch {
      setError("通信エラーが発生しました");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="w-full max-w-sm p-8 bg-white rounded-xl shadow-md">
    
handleSubmit function · typescript · L13-L37 (25 LOC)
src/app/login/page.tsx
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError("");
    setLoading(true);

    try {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });
      const data = await res.json();

      if (!res.ok) {
        setError(data.error || "ログインに失敗しました");
        return;
      }

      router.push("/dashboard");
    } catch {
      setError("通信エラーが発生しました");
    } finally {
      setLoading(false);
    }
  }
Home function · typescript · L3-L5 (3 LOC)
src/app/page.tsx
export default function Home() {
  redirect("/dashboard");
}
Want this analysis on your repo? https://repobility.com/scan/
EngagementChart function · typescript · L26-L64 (39 LOC)
src/components/analytics/EngagementChart.tsx
export default function EngagementChart({ data }: Props) {
  if (data.length === 0) {
    return (
      <div className="bg-white p-6 rounded-xl shadow-sm border text-center text-gray-500">
        エンゲージメントデータがありません
      </div>
    );
  }

  const chartData = data
    .slice(0, 20)
    .reverse()
    .map((d) => ({
      name: d.body.substring(0, 20) + (d.body.length > 20 ? "…" : ""),
      いいね: d.likeCount,
      リポスト: d.repostCount,
      リプライ: d.replyCount,
    }));

  return (
    <div className="bg-white p-6 rounded-xl shadow-sm border">
      <h3 className="text-lg font-semibold mb-4">
        投稿別エンゲージメント (Bluesky)
      </h3>
      <ResponsiveContainer width="100%" height={300}>
        <BarChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" tick={{ fontSize: 11 }} />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="いいね" fill="#ec4899" />
          <Bar dataKey="リポスト" fill="#8b5cf6" />
 
formatDate function · typescript · L24-L30 (7 LOC)
src/components/analytics/FollowerGrowthChart.tsx
function formatDate(dateStr: string) {
  return new Date(dateStr).toLocaleDateString("ja-JP", {
    timeZone: "Asia/Tokyo",
    month: "short",
    day: "numeric",
  });
}
FollowerGrowthChart function · typescript · L32-L76 (45 LOC)
src/components/analytics/FollowerGrowthChart.tsx
export default function FollowerGrowthChart({ data }: Props) {
  if (data.length === 0) {
    return (
      <div className="bg-white p-6 rounded-xl shadow-sm border text-center text-gray-500">
        フォロワーデータがありません
      </div>
    );
  }

  const chartData = data.map((d) => ({
    date: formatDate(d.collectedAt),
    フォロワー: d.followersCount,
    フォロー中: d.followsCount,
  }));

  return (
    <div className="bg-white p-6 rounded-xl shadow-sm border">
      <h3 className="text-lg font-semibold mb-4">
        フォロワー推移 (Bluesky)
      </h3>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" tick={{ fontSize: 11 }} />
          <YAxis />
          <Tooltip />
          <Line
            type="monotone"
            dataKey="フォロワー"
            stroke="#3b82f6"
            strokeWidth={2}
            dot={{ r: 3 }}
          />
          <Line
            type="monotone"
 
MetricsOverview function · typescript · L23-L39 (17 LOC)
src/components/analytics/MetricsOverview.tsx
export default function MetricsOverview({ summary }: Props) {
  return (
    <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
      {cards.map((card) => (
        <div
          key={card.key}
          className="bg-white p-4 rounded-xl shadow-sm border text-center"
        >
          <p className={`text-2xl font-bold ${card.color}`}>
            {summary[card.key].toLocaleString()}
          </p>
          <p className="text-xs text-gray-500 mt-1">{card.label}</p>
        </div>
      ))}
    </div>
  );
}
PostComposer function · typescript · L17-L241 (225 LOC)
src/components/compose/PostComposer.tsx
export default function PostComposer({ allDays, todayDate }: Props) {
  const router = useRouter();
  const [body, setBody] = useState("");
  const [platforms, setPlatforms] = useState<string[]>(["bluesky"]);
  const [scheduleMode, setScheduleMode] = useState(false);
  const [scheduledAt, setScheduledAt] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  // テンプレート選択
  const [selectedDay, setSelectedDay] = useState<string>(todayDate || "");

  const currentDay = allDays.find((d) => d.date === selectedDay);

  function selectTemplate(templateBody: string, hashtags: string) {
    const fullText = hashtags ? `${templateBody}\n\n${hashtags}` : templateBody;
    setBody(fullText);
  }

  function togglePlatform(id: string) {
    setPlatforms((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }

  const maxLength = Math.min(
    ...platforms.map(
      (id) => PLATFORMS.find((p) => p.id === id)?.
selectTemplate function · typescript · L31-L34 (4 LOC)
src/components/compose/PostComposer.tsx
  function selectTemplate(templateBody: string, hashtags: string) {
    const fullText = hashtags ? `${templateBody}\n\n${hashtags}` : templateBody;
    setBody(fullText);
  }
togglePlatform function · typescript · L36-L40 (5 LOC)
src/components/compose/PostComposer.tsx
  function togglePlatform(id: string) {
    setPlatforms((prev) =>
      prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
    );
  }
handleSave function · typescript · L48-L105 (58 LOC)
src/components/compose/PostComposer.tsx
  async function handleSave(publish: boolean) {
    setError("");
    setLoading(true);

    try {
      const createRes = await fetch("/api/posts", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          body,
          platforms,
          scheduledAt: scheduleMode && scheduledAt ? scheduledAt : null,
        }),
      });

      const post = await createRes.json();
      if (!createRes.ok) {
        setError(post.error || "作成に失敗しました");
        return;
      }

      if (publish) {
        const pubRes = await fetch("/api/posts/publish", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ postId: post.id }),
        });

        const pubData = await pubRes.json();
        if (!pubRes.ok) {
          setError(pubData.error || "投稿に失敗しました");
          return;
        }

        const failures = pubData.results.filter(
          (r: { success: boolean }
If a scraper extracted this row, it came from Repobility (https://repobility.com)
formatJST function · typescript · L32-L36 (5 LOC)
src/components/posts/PostList.tsx
function formatJST(dateStr: string) {
  return new Date(dateStr).toLocaleString("ja-JP", {
    timeZone: "Asia/Tokyo",
  });
}
PostList function · typescript · L38-L167 (130 LOC)
src/components/posts/PostList.tsx
export default function PostList() {
  const router = useRouter();
  const [postsList, setPostsList] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchPosts();
  }, []);

  async function fetchPosts() {
    const res = await fetch("/api/posts");
    const data = await res.json();
    setPostsList(data);
    setLoading(false);
  }

  async function handleDelete(id: string) {
    if (!confirm("この投稿を削除しますか?")) return;
    await fetch(`/api/posts/${id}`, { method: "DELETE" });
    setPostsList((prev) => prev.filter((p) => p.id !== id));
  }

  async function handlePublish(id: string) {
    const res = await fetch("/api/posts/publish", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ postId: id }),
    });
    if (res.ok) {
      fetchPosts();
    }
  }

  if (loading) return <div className="text-gray-500">読み込み中...</div>;
  if (postsList.length === 0)
    return <div className="tex
fetchPosts function · typescript · L47-L52 (6 LOC)
src/components/posts/PostList.tsx
  async function fetchPosts() {
    const res = await fetch("/api/posts");
    const data = await res.json();
    setPostsList(data);
    setLoading(false);
  }
handleDelete function · typescript · L54-L58 (5 LOC)
src/components/posts/PostList.tsx
  async function handleDelete(id: string) {
    if (!confirm("この投稿を削除しますか?")) return;
    await fetch(`/api/posts/${id}`, { method: "DELETE" });
    setPostsList((prev) => prev.filter((p) => p.id !== id));
  }
handlePublish function · typescript · L60-L69 (10 LOC)
src/components/posts/PostList.tsx
  async function handlePublish(id: string) {
    const res = await fetch("/api/posts/publish", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ postId: id }),
    });
    if (res.ok) {
      fetchPosts();
    }
  }
getSecret function · typescript · L7-L11 (5 LOC)
src/lib/auth/index.ts
function getSecret() {
  const secret = process.env.JWT_SECRET;
  if (!secret) throw new Error("JWT_SECRET is not set");
  return new TextEncoder().encode(secret);
}
hashPassword function · typescript · L13-L15 (3 LOC)
src/lib/auth/index.ts
export async function hashPassword(password: string): Promise<string> {
  return hash(password, 12);
}
verifyPassword function · typescript · L17-L22 (6 LOC)
src/lib/auth/index.ts
export async function verifyPassword(
  password: string,
  hashedPassword: string
): Promise<boolean> {
  return compare(password, hashedPassword);
}
All rows above produced by Repobility · https://repobility.com
createToken function · typescript · L24-L30 (7 LOC)
src/lib/auth/index.ts
export async function createToken(userId: string): Promise<string> {
  return new SignJWT({ userId })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("7d")
    .sign(getSecret());
}
verifyToken function · typescript · L32-L34 (3 LOC)
src/lib/auth/index.ts
export async function verifyToken(
  token: string
): Promise<{ userId: string } | null> {
setAuthCookie function · typescript · L43-L52 (10 LOC)
src/lib/auth/index.ts
export async function setAuthCookie(token: string) {
  const cookieStore = await cookies();
  cookieStore.set(COOKIE_NAME, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });
}
page 1 / 2next ›