A lot of things

- Added Database page.
- Added Xenforo API compatibility
- Added Hovercard
- Added Notifications
This commit is contained in:
2026-05-24 11:47:20 +02:00
parent 7cd6dfddda
commit a778222564
51 changed files with 3228 additions and 38 deletions

View File

@@ -3,6 +3,8 @@ import EasyMDE from "easymde";
import "easymde/dist/easymde.min.css";
import { calculate as calculateHashes } from "./hashes.js";
import hovercard from "./hovercard.js";
import notifications from "./notifications.js";
// Lucide icons.
@@ -19,3 +21,9 @@ window.EasyMDE = EasyMDE;
// Hashes.
window.calculateHashes = calculateHashes;
// Hover card.
Alpine.store('hovercard', hovercard() );
// Notifications
Alpine.store('notifications', notifications() );

115
resources/js/hovercard.js Normal file
View File

@@ -0,0 +1,115 @@
/** @typedef { import('types/HovercardResponse.js').HovercardResponse} HovercardResponse */
export default function hovercard(){
return {
/**
* @type {boolean}
*/
start: false,
/**
* @type {HovercardResponse}
*/
data: null,
/**
* @type {boolean}
*/
loading: false,
/**
* @type {any}
*/
error: false,
/**
* @type {HTMLElement|null}
*/
anchorEl: null,
/**
* @type {number}
*/
x: 0,
/**
* @type {number}
*/
y: 0,
/**
*
* @param {HTMLElement} anchorEl
* @param {string} fetchUrl
* @return {Promise<void>}
*/
async open(anchorEl, fetchUrl){
if( this.start && this.anchorEl === anchorEl ){
// this.close();
return;
}
this.start = true;
this.anchorEl = anchorEl;
this.data = null;
this.loading = true;
this.error = false;
this.updatePosition(anchorEl);
try {
const RESPONSE = await fetch(fetchUrl);
if( !RESPONSE.ok )
throw new Error(RESPONSE.status);
let json = await RESPONSE.json();
if( !json.user )
throw new Error(RESPONSE.status);
this.data = json.user;
Alpine.nextTick(() => {
const card = document.querySelector('.hovercard');
if (card) window.refreshIcons(card);
});
} catch( error ){
this.error = true;
} finally {
this.loading = false;
}
},
/**
* Update Hovercard position
* @param {HTMLElement} anchorEl
*/
updatePosition(anchorEl){
const RECT = anchorEl.getBoundingClientRect();
const SCROLL_X = window.scrollX;
const SCROLL_Y = window.scrollY;
let x = RECT.left + SCROLL_X;
let y = RECT.bottom + SCROLL_Y + 8;
const WIDTH = 280;
if( x + WIDTH > window.innerWidth ){
x = window.innerWidth - WIDTH - 16;
}
this.x = x;
this.y = y;
},
/**
* Close the hovercard.
*/
close(){
this.start = false;
this.data = null;
this.anchorEl = null;
this.error = false;
}
}
}

View File

@@ -0,0 +1,126 @@
/** @typedef { import('types/AlertsResponseItem.js').AlertsResponseItem} AlertsResponseItem */
export default function notifications() {
return {
/**
* @type {boolean}
*/
start: false,
/**
* @type {AlertsResponseItem[]}
*/
data: null,
/**
* @type {boolean}
*/
loading: false,
/**
* @type {boolean}
*/
error: false,
/**
* @type {number}
*/
unviewed: 0,
/**
* Request for getting notifications.
*
* @return {Promise<void>}
*/
async getNotifications() {
if( this.loading )
return;
this.loading = true;
this.error = false;
try {
const RESPONSE = await fetch('/api/dynamic/notifications', { credentials: "include", headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if( !RESPONSE.ok )
throw new Error(RESPONSE.status)
let json = await RESPONSE.json()
if( !json.alerts )
throw new Error(RESPONSE.status);
this.data = json.alerts;
} catch (error) {
this.error = true;
} finally {
this.loading = false;
}
},
/**
*
* @param {HTMLElement} anchorEl
* @return {Promise<void>}
*/
async open( anchorEl ){
if( this.start ){
this.close();
return;
}
this.start = !this.start;
if( this.start && !this.data ){
await this.getNotifications();
}
if( this.start ){
Alpine.nextTick(() => window.refreshIcons(document.querySelector('.notifications')))
}
},
/**
*
* @return {Promise<void>}
*/
async markAllRead(){
await fetch('/api/dynamic/notifications/mark-all-read', {
method: 'POST',
credentials: "include",
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '' }
});
if(this.data && this.data.length > 0){
this.data = this.data.map(a => ({
...a,
view_date: Math.floor(Date.now() / 1000)
}));
}
},
/**
* @return {number}
*/
get unread(){
if( !this.data ){
return this.unviewed;
}
return this.data.filter(a => a.view_date === 0 ).length;
},
/**
*
*/
close(){
if( this.start && this.unread > 0)
this.markAllRead();
this.start = false;
}
}
}

View File

@@ -0,0 +1,21 @@
/**
* @typedef {Object} AlertsResponseItem
*
* @see app/Http/DynamicLoadController.php
*
* @property {number} alert_id
* @property {number} alerted_user_id
* @property {number} user_id
* @property {string} username
* @property {string} content_type
* @property {number} content_id
* @property {string} action
* @property {number} event_date
* @property {number} view_date
* @property {number} read_date
* @property {boolean} auto_read
* @property {string} alert_text
* @property {string} alert_url
* @property {Object} user See XenForo API Documentation for more details.
*/
export {}

View File

@@ -0,0 +1,18 @@
/**
* @typedef {Object} HovercardResponse
*
* @see app/Http/DynamicLoadController.php
*
* @property {string} username
* @property {string|null} avatar_url
* @property {string} avatar_color
* @property {string} avatar_letter
* @property {string} group_name
* @property {string} joined
* @property {string} last_seen
* @property {number} message_count
* @property {number} reaction_score
* @property {number} trophy_points
* @property {number} entries_count
*/
export {}