774 lines
21 KiB
HTML
774 lines
21 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}AT URI Browser - {{ config.title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="pds-container">
|
|
<div class="pds-header">
|
|
</div>
|
|
|
|
<!-- User Handle Input Form -->
|
|
<div class="pds-search-section">
|
|
<form class="pds-search-form" onsubmit="searchUser(); return false;">
|
|
<div class="form-group">
|
|
<input type="text" id="handleInput" placeholder="ai.syui.ai" value="at://ai.syui.ai" />
|
|
<button type="submit" id="searchButton">
|
|
<i class="fab fa-bluesky"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Current User DID -->
|
|
<div id="userDidSection" class="user-did-section" style="display: none;">
|
|
<div class="pds-display">
|
|
<strong>PDS:</strong> <span id="userPdsText"></span>
|
|
</div>
|
|
<div class="handle-display">
|
|
<strong>Handle:</strong> <span id="userHandleText"></span>
|
|
</div>
|
|
<div class="did-display">
|
|
<span id="userDidText"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collection List -->
|
|
<div id="collectionsSection" class="collections-section" style="display: none;">
|
|
<div class="collections-header">
|
|
<button id="collectionsToggle" class="collections-toggle" onclick="toggleCollections()">[+] Collections</button>
|
|
</div>
|
|
<div id="collectionsList" class="collections-list" style="display: none;">
|
|
<!-- Collections will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AT URI Records -->
|
|
<div id="recordsSection" class="records-section" style="display: none;">
|
|
<div id="recordsList" class="records-list">
|
|
<!-- Records will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<!-- AT URI Modal -->
|
|
<div id="atUriModal" class="at-uri-modal-overlay" style="display: none;" onclick="closeAtUriModal(event)">
|
|
<div class="at-uri-modal-content">
|
|
<button class="at-uri-modal-close" onclick="closeAtUriModal()">×</button>
|
|
<div id="atUriContent"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.pds-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.pds-header {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.pds-header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
|
|
.pds-search-section {
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.pds-search-form {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
|
|
.form-group input {
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 16px;
|
|
width: 300px;
|
|
}
|
|
|
|
.form-group button {
|
|
padding: 10px 15px;
|
|
background: #1976d2;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 18px;
|
|
min-width: 50px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.form-group button:hover {
|
|
background: #1565c0;
|
|
}
|
|
|
|
.user-info {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.user-profile {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.user-details h3 {
|
|
margin: 0 0 5px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.user-details p {
|
|
margin: 0;
|
|
color: #666;
|
|
}
|
|
|
|
.user-did-section {
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.did-display {
|
|
padding: 10px;
|
|
background: #f5f5f5;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
color: #666;
|
|
word-break: break-all;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.handle-display {
|
|
padding: 8px 10px;
|
|
background: #f0f9f0;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
color: #555;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.handle-display strong {
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.handle-display span {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: #666;
|
|
word-break: break-all;
|
|
}
|
|
|
|
|
|
.pds-display {
|
|
padding: 8px 10px;
|
|
background: #e8f4f8;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
color: #555;
|
|
}
|
|
|
|
.pds-display strong {
|
|
color: #1976d2;
|
|
}
|
|
|
|
.pds-display span {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: #666;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.collections-section,
|
|
.records-section {
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.collections-section h3,
|
|
.records-section h3 {
|
|
font-size: 1.2em;
|
|
margin-bottom: 15px;
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.collections-list,
|
|
.records-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.at-uri-link {
|
|
display: block;
|
|
padding: 8px 12px;
|
|
background: #f9f9f9;
|
|
border-radius: 4px;
|
|
border: 1px solid #e0e0e0;
|
|
color: #1976d2;
|
|
text-decoration: none;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
word-break: break-all;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.at-uri-link:hover {
|
|
background: #e8f4f8;
|
|
border-color: #1976d2;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.pds-info {
|
|
padding: 8px 12px;
|
|
background: #f0f9ff;
|
|
border-radius: 4px;
|
|
border: 1px solid #b3e5fc;
|
|
margin-bottom: 8px;
|
|
color: #1976d2;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.collection-info {
|
|
padding: 8px 12px;
|
|
background: #f0f9f0;
|
|
border-radius: 4px;
|
|
border: 1px solid #b3e5b3;
|
|
margin-bottom: 8px;
|
|
color: #2e7d32;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.collections-header {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.collections-toggle {
|
|
background: #f5f5f5;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
color: #333;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.collections-toggle:hover {
|
|
background: #e8f4f8;
|
|
border-color: #1976d2;
|
|
}
|
|
|
|
|
|
.pds-test-section,
|
|
.pds-about-section {
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.pds-test-section h2,
|
|
.pds-about-section h2 {
|
|
font-size: 1.8em;
|
|
margin-bottom: 20px;
|
|
color: #333;
|
|
border-bottom: 2px solid #1976d2;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.test-uris {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.at-uri {
|
|
background: #f5f5f5;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
word-break: break-all;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.at-uri:hover {
|
|
background: #e8f4f8;
|
|
border-color: #1976d2;
|
|
}
|
|
|
|
.pds-about-section ul {
|
|
list-style-type: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.pds-about-section li {
|
|
padding: 5px 0;
|
|
color: #666;
|
|
}
|
|
|
|
|
|
/* AT URI Modal Styles */
|
|
.at-uri-modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.at-uri-modal-content {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
max-width: 800px;
|
|
max-height: 600px;
|
|
width: 90%;
|
|
height: 80%;
|
|
overflow: auto;
|
|
position: relative;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.at-uri-modal-close {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: none;
|
|
border: none;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
z-index: 1001;
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
/* Loading states */
|
|
.loading {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #666;
|
|
}
|
|
|
|
.error {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #d32f2f;
|
|
background: #ffeaea;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
/* Responsive design */
|
|
@media (max-width: 768px) {
|
|
.pds-search-form {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.form-group {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.form-group input {
|
|
width: 100%;
|
|
margin-bottom: 10px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// AT Protocol API functions
|
|
const AT_PROTOCOL_CONFIG = {
|
|
primary: {
|
|
pds: 'https://syu.is',
|
|
plc: 'https://plc.syu.is',
|
|
bsky: 'https://bsky.syu.is',
|
|
web: 'https://web.syu.is'
|
|
},
|
|
fallback: {
|
|
pds: 'https://bsky.social',
|
|
plc: 'https://plc.directory',
|
|
bsky: 'https://public.api.bsky.app',
|
|
web: 'https://bsky.app'
|
|
}
|
|
};
|
|
|
|
// Search user function
|
|
async function searchUser() {
|
|
const handleInput = document.getElementById('handleInput');
|
|
const userInfo = document.getElementById('userInfo');
|
|
const collectionsList = document.getElementById('collectionsList');
|
|
const recordsList = document.getElementById('recordsList');
|
|
const searchButton = document.getElementById('searchButton');
|
|
|
|
const input = handleInput.value.trim();
|
|
if (!input) {
|
|
alert('Handle nameまたはAT URIを入力してください');
|
|
return;
|
|
}
|
|
|
|
searchButton.disabled = true;
|
|
searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
|
|
|
|
try {
|
|
// Clear previous results
|
|
document.getElementById('userDidSection').style.display = 'none';
|
|
document.getElementById('collectionsSection').style.display = 'none';
|
|
document.getElementById('recordsSection').style.display = 'none';
|
|
collectionsList.innerHTML = '';
|
|
recordsList.innerHTML = '';
|
|
|
|
// Check if input is AT URI
|
|
if (input.startsWith('at://')) {
|
|
// Parse AT URI to check if it's a full record or just a handle/collection
|
|
const uriParts = input.replace('at://', '').split('/').filter(part => part.length > 0);
|
|
|
|
if (uriParts.length >= 3) {
|
|
// Full AT URI with rkey - show in modal
|
|
showAtUriModal(input);
|
|
return;
|
|
} else if (uriParts.length === 1) {
|
|
// Just handle in AT URI format (at://handle) - treat as regular handle
|
|
const handle = uriParts[0];
|
|
const userProfile = await resolveUserProfile(handle);
|
|
|
|
if (userProfile.success) {
|
|
displayUserDid(userProfile.data);
|
|
await loadUserCollections(handle, userProfile.data.did);
|
|
} else {
|
|
alert('ユーザーが見つかりません: ' + userProfile.error);
|
|
}
|
|
return;
|
|
} else if (uriParts.length === 2) {
|
|
// Collection level AT URI - load collection records
|
|
const [repo, collection] = uriParts;
|
|
|
|
try {
|
|
// First resolve the repo to get handle if it's a DID
|
|
let handle = repo;
|
|
if (repo.startsWith('did:')) {
|
|
// Try to resolve DID to handle - for now just use the DID
|
|
handle = repo;
|
|
}
|
|
|
|
loadCollectionRecords(handle, collection, repo);
|
|
} catch (error) {
|
|
alert('コレクションの読み込みに失敗しました: ' + error.message);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle regular handle search
|
|
const userProfile = await resolveUserProfile(input);
|
|
|
|
if (userProfile.success) {
|
|
displayUserDid(userProfile.data);
|
|
await loadUserCollections(input, userProfile.data.did);
|
|
} else {
|
|
alert('ユーザーが見つかりません: ' + userProfile.error);
|
|
}
|
|
} catch (error) {
|
|
alert('エラーが発生しました: ' + error.message);
|
|
} finally {
|
|
searchButton.disabled = false;
|
|
searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
|
|
}
|
|
}
|
|
|
|
// Resolve user profile
|
|
async function resolveUserProfile(handle) {
|
|
try {
|
|
let response = null;
|
|
|
|
// Try syu.is first
|
|
try {
|
|
response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
|
} catch (error) {
|
|
console.log('Failed to resolve from syu.is:', error);
|
|
}
|
|
|
|
// If syu.is fails, try bsky.social
|
|
if (!response || !response.ok) {
|
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to resolve handle');
|
|
}
|
|
|
|
const repoData = await response.json();
|
|
|
|
// Get profile data
|
|
const profileResponse = await fetch(`${AT_PROTOCOL_CONFIG.fallback.bsky}/xrpc/app.bsky.actor.getProfile?actor=${repoData.did}`);
|
|
const profileData = await profileResponse.json();
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
did: repoData.did,
|
|
handle: profileData.handle,
|
|
displayName: profileData.displayName,
|
|
avatar: profileData.avatar,
|
|
description: profileData.description,
|
|
pds: repoData.didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint
|
|
}
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
// Display user DID
|
|
function displayUserDid(profile) {
|
|
document.getElementById('userPdsText').textContent = profile.pds || 'Unknown';
|
|
document.getElementById('userHandleText').textContent = profile.handle;
|
|
document.getElementById('userDidText').textContent = profile.did;
|
|
document.getElementById('userDidSection').style.display = 'block';
|
|
}
|
|
|
|
// Load user collections
|
|
async function loadUserCollections(handle, did) {
|
|
const collectionsList = document.getElementById('collectionsList');
|
|
|
|
collectionsList.innerHTML = '<div class="loading">コレクションを読み込み中...</div>';
|
|
|
|
try {
|
|
// Try to get collections from describeRepo
|
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
|
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
|
|
|
|
// If syu.is fails, try bsky.social
|
|
if (!response.ok) {
|
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
|
|
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to describe repository');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const collections = data.collections || [];
|
|
|
|
// Display collections as AT URI links
|
|
collectionsList.innerHTML = '';
|
|
if (collections.length === 0) {
|
|
collectionsList.innerHTML = '<div class="error">コレクションが見つかりませんでした</div>';
|
|
} else {
|
|
|
|
collections.forEach(collection => {
|
|
const atUri = `at://${did}/${collection}/`;
|
|
const collectionElement = document.createElement('a');
|
|
collectionElement.className = 'at-uri-link';
|
|
collectionElement.href = '#';
|
|
collectionElement.textContent = atUri;
|
|
collectionElement.onclick = (e) => {
|
|
e.preventDefault();
|
|
loadCollectionRecords(handle, collection, did);
|
|
// Close collections and update toggle
|
|
document.getElementById('collectionsList').style.display = 'none';
|
|
document.getElementById('collectionsToggle').textContent = '[-] Collections';
|
|
};
|
|
collectionsList.appendChild(collectionElement);
|
|
});
|
|
|
|
document.getElementById('collectionsSection').style.display = 'block';
|
|
}
|
|
|
|
} catch (error) {
|
|
collectionsList.innerHTML = '<div class="error">コレクションの読み込みに失敗しました: ' + error.message + '</div>';
|
|
document.getElementById('collectionsSection').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Load collection records
|
|
async function loadCollectionRecords(handle, collection, did) {
|
|
const recordsList = document.getElementById('recordsList');
|
|
|
|
recordsList.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
|
|
|
|
try {
|
|
// Try with syu.is first
|
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
|
|
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
|
|
|
|
// If that fails, try with bsky.social
|
|
if (!response.ok) {
|
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
|
|
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load records');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Display records as AT URI links
|
|
recordsList.innerHTML = '';
|
|
|
|
// Add collection info for records
|
|
const collectionInfo = document.createElement('div');
|
|
collectionInfo.className = 'collection-info';
|
|
collectionInfo.innerHTML = `<strong>${collection}</strong>`;
|
|
recordsList.appendChild(collectionInfo);
|
|
|
|
data.records.forEach(record => {
|
|
const atUri = record.uri;
|
|
const recordElement = document.createElement('a');
|
|
recordElement.className = 'at-uri-link';
|
|
recordElement.href = '#';
|
|
recordElement.textContent = atUri;
|
|
recordElement.onclick = (e) => {
|
|
e.preventDefault();
|
|
showAtUriModal(atUri);
|
|
};
|
|
recordsList.appendChild(recordElement);
|
|
});
|
|
|
|
document.getElementById('recordsSection').style.display = 'block';
|
|
|
|
} catch (error) {
|
|
recordsList.innerHTML = '<div class="error">レコードの読み込みに失敗しました: ' + error.message + '</div>';
|
|
document.getElementById('recordsSection').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Show AT URI modal
|
|
function showAtUriModal(uri) {
|
|
const modal = document.getElementById('atUriModal');
|
|
const content = document.getElementById('atUriContent');
|
|
|
|
content.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
|
|
modal.style.display = 'flex';
|
|
|
|
// Load record data
|
|
loadAtUriRecord(uri, content);
|
|
}
|
|
|
|
// Load AT URI record
|
|
async function loadAtUriRecord(uri, contentElement) {
|
|
try {
|
|
const parts = uri.replace('at://', '').split('/');
|
|
const repo = parts[0];
|
|
const collection = parts[1];
|
|
const rkey = parts[2];
|
|
|
|
// Try with syu.is first
|
|
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
|
|
|
|
// If that fails, try with bsky.social
|
|
if (!response.ok) {
|
|
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load record');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
contentElement.innerHTML = `
|
|
<div style="padding: 20px;">
|
|
<h3>AT URI Record</h3>
|
|
<div style="font-family: monospace; font-size: 14px; color: #666; margin-bottom: 20px; word-break: break-all;">
|
|
${uri}
|
|
</div>
|
|
<div style="font-size: 12px; color: #999; margin-bottom: 20px;">
|
|
Repo: ${repo} | Collection: ${collection} | RKey: ${rkey}
|
|
</div>
|
|
<h4>Record Data</h4>
|
|
<pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
contentElement.innerHTML = `
|
|
<div style="padding: 20px; color: red;">
|
|
<strong>Error:</strong> ${error.message}
|
|
<div style="margin-top: 10px; font-size: 12px;">
|
|
<strong>URI:</strong> ${uri}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Close AT URI modal
|
|
function closeAtUriModal(event) {
|
|
const modal = document.getElementById('atUriModal');
|
|
if (event && event.target !== modal) {
|
|
return;
|
|
}
|
|
modal.style.display = 'none';
|
|
}
|
|
|
|
// Initialize AT URI click handlers
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Add click handlers to existing AT URIs
|
|
document.querySelectorAll('.at-uri').forEach(element => {
|
|
element.addEventListener('click', function() {
|
|
const uri = this.getAttribute('data-at-uri');
|
|
showAtUriModal(uri);
|
|
});
|
|
});
|
|
|
|
// ESC key to close modal
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeAtUriModal();
|
|
}
|
|
});
|
|
|
|
// Enter key to search
|
|
document.getElementById('handleInput').addEventListener('keydown', function(event) {
|
|
if (event.key === 'Enter') {
|
|
searchUser();
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
// Toggle collections visibility
|
|
function toggleCollections() {
|
|
const collectionsList = document.getElementById('collectionsList');
|
|
const toggleButton = document.getElementById('collectionsToggle');
|
|
|
|
if (collectionsList.style.display === 'none') {
|
|
collectionsList.style.display = 'block';
|
|
toggleButton.textContent = '[-] Collections';
|
|
} else {
|
|
collectionsList.style.display = 'none';
|
|
toggleButton.textContent = '[+] Collections';
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |