-
🔒 Please login with ATProto to use Ask AI feature
+
profile
diff --git a/oauth/src/App.css b/oauth/src/App.css
index 03439b4..bb3abce 100644
--- a/oauth/src/App.css
+++ b/oauth/src/App.css
@@ -34,6 +34,96 @@ body {
background: var(--background);
}
+/* Profile Form Styles */
+.profile-form-container {
+ background: var(--background-secondary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.profile-form-container h3 {
+ margin: 0 0 16px 0;
+ color: var(--text);
+}
+
+.profile-form .form-row {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.profile-form .form-group {
+ flex: 1;
+}
+
+.profile-form .form-group label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.profile-form .form-group input,
+.profile-form .form-group select,
+.profile-form .form-group textarea {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-size: 14px;
+ transition: border-color 0.2s;
+}
+
+.profile-form .form-group input:focus,
+.profile-form .form-group select:focus,
+.profile-form .form-group textarea:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.profile-form .form-group textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+.profile-form .submit-btn {
+ background: var(--primary);
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background-color 0.2s;
+}
+
+.profile-form .submit-btn:hover:not(:disabled) {
+ background: var(--primary-hover);
+}
+
+.profile-form .submit-btn:disabled {
+ background: var(--text-secondary);
+ cursor: not-allowed;
+}
+
+/* Profile Record List Styles */
+.profile-record-list .record-item.admin {
+ border-left: 4px solid var(--primary);
+}
+
+.profile-record-list .admin-badge {
+ background: var(--primary);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-weight: 500;
+ margin-left: 8px;
+}
+
/* Header */
.oauth-app-header {
background: var(--background);
@@ -264,7 +354,7 @@ body {
.main-content {
grid-area: main;
- max-width: 1000px;
+ max-width: 800px;
margin: 0 auto;
padding: 0px;
width: 100%;
@@ -570,13 +660,10 @@ body {
margin-bottom: 16px;
}
-.user-message {
- margin-left: 40px;
-}
-
-.ai-message {
- margin-right: 40px;
-}
+/*
+.user-message { margin-left: 40px; }
+.ai-message { margin-right: 40px; }
+*/
.message-header {
display: flex;
diff --git a/oauth/src/App.jsx b/oauth/src/App.jsx
index b684ea5..490a4b8 100644
--- a/oauth/src/App.jsx
+++ b/oauth/src/App.jsx
@@ -7,6 +7,7 @@ import { usePageContext } from './hooks/usePageContext.js'
import AuthButton from './components/AuthButton.jsx'
import RecordTabs from './components/RecordTabs.jsx'
import CommentForm from './components/CommentForm.jsx'
+import ProfileForm from './components/ProfileForm.jsx'
import AskAI from './components/AskAI.jsx'
import TestUI from './components/TestUI.jsx'
import OAuthCallback from './components/OAuthCallback.jsx'
@@ -428,6 +429,20 @@ Answer:`
)}
+ {user && (
+
+
{
+ refreshAdminData?.()
+ refreshUserData?.()
+ }}
+ />
+
+ )}
+
{
+ 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 (
+
+
プロフィール投稿
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ )
+}
+
+export default ProfileForm
\ No newline at end of file
diff --git a/oauth/src/components/ProfileRecordList.jsx b/oauth/src/components/ProfileRecordList.jsx
new file mode 100644
index 0000000..4ccfdc4
--- /dev/null
+++ b/oauth/src/components/ProfileRecordList.jsx
@@ -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 (
+
+ )
+ }
+
+ 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 (
+
+ {profileRecords.map((profile) => (
+
+
+ {profile.value.author?.avatar ? (
+

+ ) : (
+
+ {(profile.value.author?.displayName || profile.value.author?.handle || '?').charAt(0).toUpperCase()}
+
+ )}
+
+
+ {profile.value.author?.displayName || profile.value.author?.handle}
+ {profile.value.profileType === 'admin' && (
+ Admin
+ )}
+
+
@{profile.value.author?.handle}
+
{new Date(profile.value.createdAt).toLocaleString()}
+
+ {canDelete(profile) && (
+
+
+
+ )}
+
+
{profile.value.text}
+
+ ))}
+
+ )
+}
\ No newline at end of file
diff --git a/oauth/src/components/RecordTabs.jsx b/oauth/src/components/RecordTabs.jsx
index 514d371..9bf8d4a 100644
--- a/oauth/src/components/RecordTabs.jsx
+++ b/oauth/src/components/RecordTabs.jsx
@@ -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 (
-
+
+
@@ -121,6 +143,19 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
/>
)
)}
+ {activeTab === 'profiles' && (
+ !baseRecords ? (
+
+ ) : (
+
+ )
+ )}