test blog profile
This commit is contained in:
@ -170,7 +170,7 @@ a.view-markdown:any-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ask-ai-content {
|
.ask-ai-content {
|
||||||
max-width: 1000px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ a.view-markdown:any-link {
|
|||||||
/* Main Content */
|
/* Main Content */
|
||||||
.main-content {
|
.main-content {
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
max-width: 1000px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -969,6 +969,41 @@ article.article-content {
|
|||||||
.question-form {
|
.question-form {
|
||||||
padding: 12px !important;
|
padding: 12px !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Display Styles */
|
||||||
|
.profile-avatar-fallback {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--theme-color);
|
||||||
|
color: var(--white);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
background: var(--theme-color);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message, .error-message, .no-profiles {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--dark-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
|
@ -37,10 +37,81 @@ function checkAuthenticationStatus() {
|
|||||||
document.getElementById('aiQuestion').focus();
|
document.getElementById('aiQuestion').focus();
|
||||||
}, 50);
|
}, 50);
|
||||||
} else {
|
} else {
|
||||||
// User not authenticated - show auth message
|
// User not authenticated - show profiles instead of auth message
|
||||||
document.getElementById('authCheck').style.display = 'block';
|
document.getElementById('authCheck').style.display = 'none';
|
||||||
document.getElementById('chatForm').style.display = 'none';
|
document.getElementById('chatForm').style.display = 'none';
|
||||||
document.getElementById('chatHistory').style.display = 'none';
|
document.getElementById('chatHistory').style.display = 'block';
|
||||||
|
loadAndShowProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and display profiles from ai.syui.log.profile collection
|
||||||
|
async function loadAndShowProfiles() {
|
||||||
|
const chatHistory = document.getElementById('chatHistory');
|
||||||
|
chatHistory.innerHTML = '<div class="loading-message">Loading profiles...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ADMIN_HANDLE = 'ai.syui.ai';
|
||||||
|
const OAUTH_COLLECTION = 'ai.syui.log';
|
||||||
|
const ATPROTO_PDS = 'syu.is';
|
||||||
|
|
||||||
|
const response = await fetch(`https://${ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${ADMIN_HANDLE}&collection=${OAUTH_COLLECTION}&limit=100`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch profiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Fetched records:', data.records);
|
||||||
|
|
||||||
|
// Filter only profile records and sort
|
||||||
|
const profileRecords = (data.records || []).filter(record => record.value.type === 'profile');
|
||||||
|
console.log('Profile records:', profileRecords);
|
||||||
|
|
||||||
|
const profiles = 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;
|
||||||
|
});
|
||||||
|
console.log('Sorted profiles:', profiles);
|
||||||
|
|
||||||
|
// Clear loading message
|
||||||
|
chatHistory.innerHTML = '';
|
||||||
|
|
||||||
|
// Display profiles using the same format as chat
|
||||||
|
profiles.forEach(profile => {
|
||||||
|
const profileDiv = document.createElement('div');
|
||||||
|
profileDiv.className = 'chat-message ai-message comment-style';
|
||||||
|
|
||||||
|
const avatarElement = profile.value.author.avatar
|
||||||
|
? `<img src="${profile.value.author.avatar}" alt="${profile.value.author.displayName || profile.value.author.handle}" class="profile-avatar">`
|
||||||
|
: `<div class="profile-avatar-fallback">${(profile.value.author.displayName || profile.value.author.handle || '?').charAt(0).toUpperCase()}</div>`;
|
||||||
|
|
||||||
|
const adminBadge = profile.value.profileType === 'admin'
|
||||||
|
? '<span class="admin-badge">Admin</span>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
profileDiv.innerHTML = `
|
||||||
|
<div class="message-header">
|
||||||
|
<div class="avatar">${avatarElement}</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="display-name">${profile.value.author.displayName || profile.value.author.handle} ${adminBadge}</div>
|
||||||
|
<div class="handle">@${profile.value.author.handle}</div>
|
||||||
|
<div class="timestamp">${new Date(profile.value.createdAt).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">${profile.value.text}</div>
|
||||||
|
`;
|
||||||
|
chatHistory.appendChild(profileDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profiles.length === 0) {
|
||||||
|
chatHistory.innerHTML = '<div class="no-profiles">No profiles available</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading profiles:', error);
|
||||||
|
chatHistory.innerHTML = '<div class="error-message">Failed to load profiles. Please try again later.</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
|
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
|
||||||
<div class="ask-ai-content">
|
<div class="ask-ai-content">
|
||||||
<div id="authCheck" class="auth-check">
|
<div id="authCheck" class="auth-check">
|
||||||
<p>🔒 Please login with ATProto to use Ask AI feature</p>
|
<p>profile</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="chatForm" class="ask-ai-form" style="display: none;">
|
<div id="chatForm" class="ask-ai-form" style="display: none;">
|
||||||
|
@ -34,6 +34,96 @@ body {
|
|||||||
background: var(--background);
|
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 */
|
/* Header */
|
||||||
.oauth-app-header {
|
.oauth-app-header {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
@ -264,7 +354,7 @@ body {
|
|||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
max-width: 1000px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -570,13 +660,10 @@ body {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message {
|
/*
|
||||||
margin-left: 40px;
|
.user-message { margin-left: 40px; }
|
||||||
}
|
.ai-message { margin-right: 40px; }
|
||||||
|
*/
|
||||||
.ai-message {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-header {
|
.message-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -7,6 +7,7 @@ import { usePageContext } from './hooks/usePageContext.js'
|
|||||||
import AuthButton from './components/AuthButton.jsx'
|
import AuthButton from './components/AuthButton.jsx'
|
||||||
import RecordTabs from './components/RecordTabs.jsx'
|
import RecordTabs from './components/RecordTabs.jsx'
|
||||||
import CommentForm from './components/CommentForm.jsx'
|
import CommentForm from './components/CommentForm.jsx'
|
||||||
|
import ProfileForm from './components/ProfileForm.jsx'
|
||||||
import AskAI from './components/AskAI.jsx'
|
import AskAI from './components/AskAI.jsx'
|
||||||
import TestUI from './components/TestUI.jsx'
|
import TestUI from './components/TestUI.jsx'
|
||||||
import OAuthCallback from './components/OAuthCallback.jsx'
|
import OAuthCallback from './components/OAuthCallback.jsx'
|
||||||
@ -428,6 +429,20 @@ Answer:`
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div className="profile-form">
|
||||||
|
<ProfileForm
|
||||||
|
user={user}
|
||||||
|
agent={agent}
|
||||||
|
apiConfig={adminData.apiConfig}
|
||||||
|
onProfilePosted={() => {
|
||||||
|
refreshAdminData?.()
|
||||||
|
refreshUserData?.()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<RecordTabs
|
<RecordTabs
|
||||||
langRecords={langRecords}
|
langRecords={langRecords}
|
||||||
commentRecords={commentRecords}
|
commentRecords={commentRecords}
|
||||||
|
@ -58,7 +58,8 @@ async function request(url, options = {}) {
|
|||||||
|
|
||||||
export const atproto = {
|
export const atproto = {
|
||||||
async getDid(pds, handle) {
|
async getDid(pds, handle) {
|
||||||
const res = await request(`https://${pds}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`)
|
const endpoint = pds.startsWith('http') ? pds : `https://${pds}`
|
||||||
|
const res = await request(`${endpoint}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`)
|
||||||
return res.did
|
return res.did
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -179,6 +180,16 @@ export const collections = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getProfiles(pds, repo, collection, limit = 100) {
|
||||||
|
const cacheKey = dataCache.generateKey('profiles', pds, repo, collection, limit)
|
||||||
|
const cached = dataCache.get(cacheKey)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const data = await atproto.getRecords(pds, repo, `${collection}.profile`, limit)
|
||||||
|
dataCache.set(cacheKey, data)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
// 投稿後にキャッシュを無効化
|
// 投稿後にキャッシュを無効化
|
||||||
invalidateCache(collection) {
|
invalidateCache(collection) {
|
||||||
dataCache.invalidatePattern(collection)
|
dataCache.invalidatePattern(collection)
|
||||||
|
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 React, { useState } from 'react'
|
||||||
import RecordList from './RecordList.jsx'
|
import RecordList from './RecordList.jsx'
|
||||||
import ChatRecordList from './ChatRecordList.jsx'
|
import ChatRecordList from './ChatRecordList.jsx'
|
||||||
|
import ProfileRecordList from './ProfileRecordList.jsx'
|
||||||
import LoadingSkeleton from './LoadingSkeleton.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 }) {
|
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
|
// Filter records based on page context
|
||||||
const filterRecords = (records) => {
|
const filterRecords = (records) => {
|
||||||
@ -33,20 +37,26 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
|||||||
const filteredChatRecords = filterRecords(chatRecords || [])
|
const filteredChatRecords = filterRecords(chatRecords || [])
|
||||||
const filteredBaseRecords = filterRecords(baseRecords || [])
|
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 (
|
return (
|
||||||
<div className="record-tabs">
|
<div className="record-tabs">
|
||||||
<div className="tab-header">
|
<div className="tab-header">
|
||||||
<button
|
<button
|
||||||
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
|
className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('comment')}
|
onClick={() => {
|
||||||
|
logger.log('RecordTabs: Profiles tab clicked')
|
||||||
|
setActiveTab('profiles')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
feedback ({filteredCommentRecords.length})
|
about ({filteredProfileRecords.length})
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`tab-btn ${activeTab === 'lang' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('lang')}
|
|
||||||
>
|
|
||||||
en ({filteredLangRecords.length})
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
|
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
|
||||||
@ -60,6 +70,18 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
|||||||
>
|
>
|
||||||
comment ({filteredUserComments.length})
|
comment ({filteredUserComments.length})
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div className="tab-content">
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user