diff --git a/repos_extra/frontpage/packages/frontpage/app/(app)/layout.tsx b/repos_extra/frontpage/packages/frontpage/app/(app)/layout.tsx
new file mode 100644
index 0000000..e12a508
--- /dev/null
+++ b/repos_extra/frontpage/packages/frontpage/app/(app)/layout.tsx
@@ -0,0 +1,4 @@
+
+
diff --git a/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_action.ts b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_action.ts
new file mode 100644
index 0000000..74cbba0
--- /dev/null
+++ b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_action.ts
@@ -0,0 +1,26 @@
+"use server";
+
+import { DID } from "@/lib/data/atproto/did";
+import { getVerifiedHandle } from "@/lib/data/atproto/identity";
+import { putPost } from "@/lib/data/atproto/game";
+import { uncached_doesPostExist } from "@/lib/data/db/post";
+import { DataLayerError } from "@/lib/data/error";
+import { ensureUser } from "@/lib/data/user";
+import { redirect } from "next/navigation";
+
+export async function newPostAction(_prevState: unknown) {
+ "use server";
+ const user = await ensureUser();
+ const [handle] = await Promise.all([
+ getVerifiedHandle(user.did),
+ ]);
+
+ const account = `at://did:plc:4hqjfn7m6n5hno3doamuhgef/ai.syui.game.user/${handle}`;
+ try {
+ const { rkey } = await putPost({ account });
+ redirect(`https://at.syu.is/at/${user.did}/ai.syui.game/self`);
+ } catch (error) {
+ if (!(error instanceof DataLayerError)) throw error;
+ return { error: "Failed to create post" };
+ }
+}
diff --git a/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_client.tsx b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_client.tsx
new file mode 100644
index 0000000..8799951
--- /dev/null
+++ b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/_client.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { startTransition, useActionState, useId, useState } from "react";
+import { newPostAction } from "./_action";
+import { Label } from "@/lib/components/ui/label";
+import { Input } from "@/lib/components/ui/input";
+import { Button } from "@/lib/components/ui/button";
+import { Alert, AlertDescription, AlertTitle } from "@/lib/components/ui/alert";
+import { Spinner } from "@/lib/components/ui/spinner";
+import { InputLengthIndicator } from "@/lib/components/input-length-indicator";
+
+export function NewPostButton() {
+ const [state, action, isPending] = useActionState(newPostAction, null);
+ const [error, setError] = useState(null);
+
+ const handleClick = async () => {
+ try {
+ const result = await action({});
+ if (result && 'error' in result) {
+ setError(result.error);
+ }
+ } catch (e) {
+ console.error('Error creating post:', e);
+ setError('予期しないエラーが発生しました');
+ }
+ };
+
+ return (
+
+
+ {error &&
{error}
}
+
+ );
+}
diff --git a/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/page.tsx b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/page.tsx
new file mode 100644
index 0000000..adea423
--- /dev/null
+++ b/repos_extra/frontpage/packages/frontpage/app/(app)/post/game/page.tsx
@@ -0,0 +1,17 @@
+import { Metadata } from "next";
+import { NewPostButton } from "./_client";
+
+export const metadata: Metadata = {
+ title: "New post | Frontpage",
+ robots: "noindex, nofollow",
+};
+
+export default function NewPost() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/repos_extra/frontpage/packages/frontpage/lib/data/atproto/game.ts b/repos_extra/frontpage/packages/frontpage/lib/data/atproto/game.ts
new file mode 100644
index 0000000..7527a0a
--- /dev/null
+++ b/repos_extra/frontpage/packages/frontpage/lib/data/atproto/game.ts
@@ -0,0 +1,62 @@
+import "server-only";
+import {
+ atprotoPutRecord,
+ atprotoCreateRecord,
+ atprotoDeleteRecord,
+ atprotoGetRecord,
+} from "./record";
+import { z } from "zod";
+import { DataLayerError } from "../error";
+import { DID, getPdsUrl } from "./did";
+
+export const PostCollection = "ai.syui.game";
+
+export const PostRecord = z.object({
+ account: z.string(),
+ createdAt: z.string(),
+});
+
+export type Post = z.infer;
+
+type PostInput = {
+ account: string;
+};
+
+export async function putPost({ account }: PostInput) {
+ const record = { account, createdAt: new Date().toISOString() };
+ PostRecord.parse(record);
+
+ const result = await atprotoPutRecord({
+ rkey: "self",
+ record,
+ collection: PostCollection,
+ });
+
+ return {
+ rkey: "self",
+ };
+}
+
+export async function deletePost(rkey: string) {
+ await atprotoDeleteRecord({
+ rkey,
+ collection: PostCollection,
+ });
+}
+
+export async function getPost({ rkey, repo }: { rkey: string; repo: DID }) {
+ const service = await getPdsUrl(repo);
+
+ if (!service) {
+ throw new DataLayerError("Failed to get service url");
+ }
+
+ const { value } = await atprotoGetRecord({
+ serviceEndpoint: service,
+ repo,
+ collection: PostCollection,
+ rkey,
+ });
+
+ return PostRecord.parse(value);
+}