add game
This commit is contained in:
		| @@ -0,0 +1,4 @@ | |||||||
|  |             <Button asChild> | ||||||
|  |               <Link href="/post/game">Game</Link> | ||||||
|  |             </Button> | ||||||
|  |  | ||||||
| @@ -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" }; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -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<string | null>(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 ( | ||||||
|  |     <div> | ||||||
|  |       <button onClick={handleClick} disabled={isPending}> | ||||||
|  |         {isPending ? '投稿中...' : '新規投稿'} | ||||||
|  |       </button> | ||||||
|  |       {error && <p style={{ color: 'red' }}>{error}</p>} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -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 ( | ||||||
|  |     <main className="flex flex-col gap-3"> | ||||||
|  |       <h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100"> | ||||||
|  |       </h2> | ||||||
|  |       <NewPostButton /> | ||||||
|  |     </main> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -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<typeof PostRecord>; | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user