test blog profile
This commit is contained in:
165
oauth/src/components/ProfileForm.jsx
Normal file
165
oauth/src/components/ProfileForm.jsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React, { useState } from 'react'
|
||||
import { atproto, collections } from '../api/atproto.js'
|
||||
import { env } from '../config/env.js'
|
||||
|
||||
const ProfileForm = ({ user, agent, apiConfig, onProfilePosted }) => {
|
||||
const [text, setText] = useState('')
|
||||
const [type, setType] = useState('user')
|
||||
const [handle, setHandle] = useState('')
|
||||
const [rkey, setRkey] = useState('')
|
||||
const [posting, setPosting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!text.trim() || !handle.trim() || !rkey.trim()) {
|
||||
setError('すべてのフィールドを入力してください')
|
||||
return
|
||||
}
|
||||
|
||||
setPosting(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
// Get handle information
|
||||
let authorData
|
||||
try {
|
||||
const handleDid = await atproto.getDid(apiConfig.pds, handle)
|
||||
// Use agent to get profile with authentication
|
||||
const profileResponse = await agent.api.app.bsky.actor.getProfile({ actor: handleDid })
|
||||
authorData = profileResponse.data
|
||||
} catch (err) {
|
||||
throw new Error('ハンドルが見つかりません')
|
||||
}
|
||||
|
||||
// Create record using the same pattern as CommentForm
|
||||
const timestamp = new Date().toISOString()
|
||||
const record = {
|
||||
repo: user.did,
|
||||
collection: env.collection,
|
||||
rkey: rkey,
|
||||
record: {
|
||||
$type: env.collection,
|
||||
text: text,
|
||||
type: 'profile',
|
||||
profileType: type, // admin or user
|
||||
author: {
|
||||
did: authorData.did,
|
||||
handle: authorData.handle,
|
||||
displayName: authorData.displayName || authorData.handle,
|
||||
avatar: authorData.avatar || null
|
||||
},
|
||||
createdAt: timestamp,
|
||||
post: {
|
||||
url: window.location.origin,
|
||||
date: timestamp,
|
||||
slug: '',
|
||||
tags: [],
|
||||
title: 'Profile',
|
||||
language: 'ja'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post the record using agent like CommentForm
|
||||
await agent.api.com.atproto.repo.putRecord(record)
|
||||
|
||||
// Invalidate cache and refresh
|
||||
collections.invalidateCache(env.collection)
|
||||
|
||||
// Reset form
|
||||
setText('')
|
||||
setType('user')
|
||||
setHandle('')
|
||||
setRkey('')
|
||||
|
||||
if (onProfilePosted) {
|
||||
onProfilePosted()
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to create profile:', err)
|
||||
setError(err.message || 'プロフィールの作成に失敗しました')
|
||||
} finally {
|
||||
setPosting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="profile-form-container">
|
||||
<h3>プロフィール投稿</h3>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="profile-form">
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="handle">ハンドル</label>
|
||||
<input
|
||||
type="text"
|
||||
id="handle"
|
||||
value={handle}
|
||||
onChange={(e) => setHandle(e.target.value)}
|
||||
placeholder="例: syui.ai"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="rkey">Rkey</label>
|
||||
<input
|
||||
type="text"
|
||||
id="rkey"
|
||||
value={rkey}
|
||||
onChange={(e) => setRkey(e.target.value)}
|
||||
placeholder="例: syui"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="type">タイプ</label>
|
||||
<select
|
||||
id="type"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="text">プロフィールテキスト</label>
|
||||
<textarea
|
||||
id="text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="プロフィールの説明を入力してください"
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={posting || !text.trim() || !handle.trim() || !rkey.trim()}
|
||||
className="submit-btn"
|
||||
>
|
||||
{posting ? '投稿中...' : '投稿'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileForm
|
81
oauth/src/components/ProfileRecordList.jsx
Normal file
81
oauth/src/components/ProfileRecordList.jsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ProfileRecordList({ profileRecords, apiConfig, user = null, agent = null, onRecordDeleted = null }) {
|
||||
if (!profileRecords || profileRecords.length === 0) {
|
||||
return (
|
||||
<section>
|
||||
<p>プロフィールがありません</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const handleDelete = async (profile) => {
|
||||
if (!user || !agent || !profile.uri) return
|
||||
|
||||
const confirmed = window.confirm('このプロフィールを削除しますか?')
|
||||
if (!confirmed) return
|
||||
|
||||
try {
|
||||
const uriParts = profile.uri.split('/')
|
||||
await agent.api.com.atproto.repo.deleteRecord({
|
||||
repo: uriParts[2],
|
||||
collection: uriParts[3],
|
||||
rkey: uriParts[4]
|
||||
})
|
||||
|
||||
if (onRecordDeleted) {
|
||||
onRecordDeleted()
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`削除に失敗しました: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const canDelete = (profile) => {
|
||||
return user && agent && profile.uri && profile.value.author?.did === user.did
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
{profileRecords.map((profile) => (
|
||||
<div key={profile.uri} className="chat-message comment-style">
|
||||
<div className="message-header">
|
||||
{profile.value.author?.avatar ? (
|
||||
<img
|
||||
src={profile.value.author.avatar}
|
||||
alt={`${profile.value.author.displayName || profile.value.author.handle} avatar`}
|
||||
className="avatar"
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar">
|
||||
{(profile.value.author?.displayName || profile.value.author?.handle || '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="user-info">
|
||||
<div className="display-name">
|
||||
{profile.value.author?.displayName || profile.value.author?.handle}
|
||||
{profile.value.profileType === 'admin' && (
|
||||
<span className="admin-badge"> Admin</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="handle">@{profile.value.author?.handle}</div>
|
||||
<div className="timestamp">{new Date(profile.value.createdAt).toLocaleString()}</div>
|
||||
</div>
|
||||
{canDelete(profile) && (
|
||||
<div className="record-actions">
|
||||
<button
|
||||
onClick={() => handleDelete(profile)}
|
||||
className="btn btn-danger btn-sm"
|
||||
title="Delete Profile"
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="message-content">{profile.value.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import React, { useState } from 'react'
|
||||
import RecordList from './RecordList.jsx'
|
||||
import ChatRecordList from './ChatRecordList.jsx'
|
||||
import ProfileRecordList from './ProfileRecordList.jsx'
|
||||
import LoadingSkeleton from './LoadingSkeleton.jsx'
|
||||
import { logger } from '../utils/logger.js'
|
||||
|
||||
export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, userChatRecords, userChatLoading, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) {
|
||||
const [activeTab, setActiveTab] = useState('comment')
|
||||
const [activeTab, setActiveTab] = useState('profiles')
|
||||
|
||||
logger.log('RecordTabs: activeTab is', activeTab)
|
||||
|
||||
// Filter records based on page context
|
||||
const filterRecords = (records) => {
|
||||
@ -32,21 +36,27 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
const filteredUserComments = filterRecords(userComments || [])
|
||||
const filteredChatRecords = filterRecords(chatRecords || [])
|
||||
const filteredBaseRecords = filterRecords(baseRecords || [])
|
||||
|
||||
// Filter profile records from baseRecords
|
||||
const profileRecords = (baseRecords || []).filter(record => record.value?.type === 'profile')
|
||||
const sortedProfileRecords = profileRecords.sort((a, b) => {
|
||||
if (a.value.profileType === 'admin' && b.value.profileType !== 'admin') return -1
|
||||
if (a.value.profileType !== 'admin' && b.value.profileType === 'admin') return 1
|
||||
return 0
|
||||
})
|
||||
const filteredProfileRecords = filterRecords(sortedProfileRecords)
|
||||
|
||||
return (
|
||||
<div className="record-tabs">
|
||||
<div className="tab-header">
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('comment')}
|
||||
className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
logger.log('RecordTabs: Profiles tab clicked')
|
||||
setActiveTab('profiles')
|
||||
}}
|
||||
>
|
||||
feedback ({filteredCommentRecords.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'lang' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('lang')}
|
||||
>
|
||||
en ({filteredLangRecords.length})
|
||||
about ({filteredProfileRecords.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
|
||||
@ -60,6 +70,18 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
>
|
||||
comment ({filteredUserComments.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('comment')}
|
||||
>
|
||||
feedback ({filteredCommentRecords.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'lang' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('lang')}
|
||||
>
|
||||
en ({filteredLangRecords.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-content">
|
||||
@ -121,6 +143,19 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{activeTab === 'profiles' && (
|
||||
!baseRecords ? (
|
||||
<LoadingSkeleton count={3} showTitle={true} />
|
||||
) : (
|
||||
<ProfileRecordList
|
||||
profileRecords={filteredProfileRecords}
|
||||
apiConfig={apiConfig}
|
||||
user={user}
|
||||
agent={agent}
|
||||
onRecordDeleted={onRecordDeleted}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user