Function bodies 68 total
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),
gtGET 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)
upDELETE 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 NextRespoPUT 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 reWant 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="texfetchPosts 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 ›