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); +}