test blog profile
This commit is contained in:
		| @@ -170,7 +170,7 @@ a.view-markdown:any-link { | ||||
| } | ||||
|  | ||||
| .ask-ai-content { | ||||
|     max-width: 1000px; | ||||
|     max-width: 800px; | ||||
|     margin: 0 auto; | ||||
| } | ||||
|  | ||||
| @@ -200,7 +200,7 @@ a.view-markdown:any-link { | ||||
| /* Main Content */ | ||||
| .main-content { | ||||
| 	grid-area: main; | ||||
| 	max-width: 1000px; | ||||
| 	max-width: 800px; | ||||
| 	margin: 0 auto; | ||||
| 	padding: 0px; | ||||
| 	width: 100%; | ||||
| @@ -969,6 +969,41 @@ article.article-content { | ||||
|     .question-form { | ||||
|         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 { | ||||
|         flex-direction: column !important; | ||||
|   | ||||
| @@ -37,10 +37,81 @@ function checkAuthenticationStatus() { | ||||
|             document.getElementById('aiQuestion').focus(); | ||||
|         }, 50); | ||||
|     } else { | ||||
|         // User not authenticated - show auth message | ||||
|         document.getElementById('authCheck').style.display = 'block'; | ||||
|         // User not authenticated - show profiles instead of auth message | ||||
|         document.getElementById('authCheck').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-content"> | ||||
|                 <div id="authCheck" class="auth-check"> | ||||
|                     <p>🔒 Please login with ATProto to use Ask AI feature</p> | ||||
|                     <p>profile</p> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div id="chatForm" class="ask-ai-form" style="display: none;"> | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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:` | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {user && ( | ||||
|             <div className="profile-form"> | ||||
|               <ProfileForm | ||||
|                 user={user} | ||||
|                 agent={agent} | ||||
|                 apiConfig={adminData.apiConfig} | ||||
|                 onProfilePosted={() => { | ||||
|                   refreshAdminData?.() | ||||
|                   refreshUserData?.() | ||||
|                 }} | ||||
|               /> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           <RecordTabs  | ||||
|             langRecords={langRecords} | ||||
|             commentRecords={commentRecords} | ||||
|   | ||||
| @@ -58,7 +58,8 @@ async function request(url, options = {}) { | ||||
|  | ||||
| export const atproto = { | ||||
|   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 | ||||
|   }, | ||||
|  | ||||
| @@ -179,6 +180,16 @@ export const collections = { | ||||
|     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) { | ||||
|     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 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