feat(plugins): expose plugin routes and load FurryPlace SDK
Add setupPluginRoutes to the server and register it in index.ts. Include /furryplace-sdk.js on admin, index and moderation backup pages to enable custom button extensions.
This commit is contained in:
@@ -88,6 +88,9 @@
|
|||||||
href="./img/apple-touch-icon.png"
|
href="./img/apple-touch-icon.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="./site.webmanifest" />
|
<link rel="manifest" href="./site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- FurryPlace SDK for custom button extensions -->
|
||||||
|
<script src="/furryplace-sdk.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* FurryPlace SDK - Button Extension API
|
||||||
|
*
|
||||||
|
* This SDK allows external scripts to add custom buttons to the FurryPlace UI.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Register a custom button
|
||||||
|
* window.FurryPlaceSDK.registerButton({
|
||||||
|
* id: 'my-custom-button',
|
||||||
|
* title: 'My Button',
|
||||||
|
* icon: '<svg>...</svg>',
|
||||||
|
* position: 'before-leaderboard', // or 'after-leaderboard', 'top', 'bottom'
|
||||||
|
* onClick: (context) => {
|
||||||
|
* console.log('Button clicked!', context);
|
||||||
|
* },
|
||||||
|
* condition: (context) => {
|
||||||
|
* // Optional: only show when certain conditions are met
|
||||||
|
* return context.user?.isLoggedIn;
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const SDK_VERSION = '1.0.0';
|
||||||
|
const registeredButtons = [];
|
||||||
|
let isInitialized = false;
|
||||||
|
let injectionPoint = null;
|
||||||
|
|
||||||
|
// Find the button container in the DOM
|
||||||
|
function findButtonContainer() {
|
||||||
|
// Look for the container with class "flex flex-col items-center gap-3"
|
||||||
|
const containers = document.querySelectorAll('.flex.flex-col.items-center.gap-3');
|
||||||
|
for (const container of containers) {
|
||||||
|
// Verify it contains the expected buttons by checking for specific classes
|
||||||
|
const hasExpectedButtons = container.querySelector('.btn.btn-square.shadow-md');
|
||||||
|
if (hasExpectedButtons) {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the DOM to contain the button container
|
||||||
|
function waitForButtonContainer(callback, maxAttempts = 50) {
|
||||||
|
let attempts = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const container = findButtonContainer();
|
||||||
|
if (container) {
|
||||||
|
clearInterval(interval);
|
||||||
|
callback(container);
|
||||||
|
} else if (++attempts >= maxAttempts) {
|
||||||
|
clearInterval(interval);
|
||||||
|
console.error('[FurryPlace SDK] Could not find button container after', maxAttempts, 'attempts');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a button element
|
||||||
|
function createButton(config) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.setAttribute('data-furryplace-button', config.id);
|
||||||
|
wrapper.className = config.wrapperClass || '';
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = config.className || 'btn btn-square shadow-md';
|
||||||
|
button.title = config.title || '';
|
||||||
|
|
||||||
|
if (config.disabled) {
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add icon
|
||||||
|
if (config.icon) {
|
||||||
|
if (typeof config.icon === 'string') {
|
||||||
|
button.innerHTML = config.icon;
|
||||||
|
} else if (config.icon instanceof HTMLElement) {
|
||||||
|
button.appendChild(config.icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
if (config.onClick) {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
config.onClick(context, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(button);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context information for button callbacks
|
||||||
|
function getContext() {
|
||||||
|
return {
|
||||||
|
sdk: {
|
||||||
|
version: SDK_VERSION,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// This would need to be populated from the app's state
|
||||||
|
// For now, return null - plugins can access app state directly if needed
|
||||||
|
isLoggedIn: false,
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
// Map context if available
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject buttons into the DOM
|
||||||
|
function injectButtons(container) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
registeredButtons.forEach(config => {
|
||||||
|
// Check condition
|
||||||
|
if (config.condition) {
|
||||||
|
const context = getContext();
|
||||||
|
if (!config.condition(context)) {
|
||||||
|
return; // Skip this button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = createButton(config);
|
||||||
|
|
||||||
|
// Determine insertion position
|
||||||
|
switch (config.position) {
|
||||||
|
case 'top':
|
||||||
|
container.insertBefore(button, container.firstChild);
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
container.appendChild(button);
|
||||||
|
break;
|
||||||
|
case 'before-leaderboard': {
|
||||||
|
// Find leaderboard button (has specific SVG viewBox)
|
||||||
|
const leaderboardBtn = Array.from(container.querySelectorAll('button')).find(btn => {
|
||||||
|
const svg = btn.querySelector('svg[viewBox="0 -960 960 960"]');
|
||||||
|
return svg && svg.querySelector('path[d*="160-200h160v-320"]');
|
||||||
|
});
|
||||||
|
if (leaderboardBtn && leaderboardBtn.parentElement) {
|
||||||
|
container.insertBefore(button, leaderboardBtn.parentElement);
|
||||||
|
} else {
|
||||||
|
container.appendChild(button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'after-leaderboard': {
|
||||||
|
const leaderboardBtn = Array.from(container.querySelectorAll('button')).find(btn => {
|
||||||
|
const svg = btn.querySelector('svg[viewBox="0 -960 960 960"]');
|
||||||
|
return svg && svg.querySelector('path[d*="160-200h160v-320"]');
|
||||||
|
});
|
||||||
|
if (leaderboardBtn && leaderboardBtn.parentElement && leaderboardBtn.parentElement.nextSibling) {
|
||||||
|
container.insertBefore(button, leaderboardBtn.parentElement.nextSibling);
|
||||||
|
} else {
|
||||||
|
container.appendChild(button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
container.appendChild(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
injectionPoint = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-inject buttons (useful when the app re-renders)
|
||||||
|
function reinjectButtons() {
|
||||||
|
if (!injectionPoint) return;
|
||||||
|
|
||||||
|
// Remove old injected buttons
|
||||||
|
const oldButtons = injectionPoint.querySelectorAll('[data-furryplace-button]');
|
||||||
|
oldButtons.forEach(btn => btn.remove());
|
||||||
|
|
||||||
|
// Re-inject
|
||||||
|
injectButtons(injectionPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the SDK
|
||||||
|
function initialize() {
|
||||||
|
if (isInitialized) {
|
||||||
|
console.warn('[FurryPlace SDK] Already initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FurryPlace SDK] Initializing v' + SDK_VERSION);
|
||||||
|
|
||||||
|
waitForButtonContainer((container) => {
|
||||||
|
console.log('[FurryPlace SDK] Found button container, injecting buttons...');
|
||||||
|
injectButtons(container);
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
// Set up mutation observer to re-inject on DOM changes
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const currentContainer = findButtonContainer();
|
||||||
|
if (currentContainer && currentContainer !== injectionPoint) {
|
||||||
|
// Container changed, re-inject
|
||||||
|
injectButtons(currentContainer);
|
||||||
|
} else if (currentContainer && injectionPoint) {
|
||||||
|
// Check if our buttons are still there
|
||||||
|
const ourButtons = currentContainer.querySelectorAll('[data-furryplace-button]');
|
||||||
|
if (ourButtons.length !== registeredButtons.length) {
|
||||||
|
reinjectButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
window.FurryPlaceSDK = {
|
||||||
|
version: SDK_VERSION,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom button
|
||||||
|
* @param {Object} config - Button configuration
|
||||||
|
* @param {string} config.id - Unique identifier for the button
|
||||||
|
* @param {string} config.title - Tooltip text
|
||||||
|
* @param {string|HTMLElement} config.icon - SVG string or element
|
||||||
|
* @param {Function} config.onClick - Click handler (context, event) => {}
|
||||||
|
* @param {string} [config.position='bottom'] - Where to place the button
|
||||||
|
* @param {string} [config.className] - Custom CSS classes
|
||||||
|
* @param {string} [config.wrapperClass] - Wrapper div CSS classes
|
||||||
|
* @param {Function} [config.condition] - Optional condition (context) => boolean
|
||||||
|
* @param {boolean} [config.disabled=false] - Whether button is disabled
|
||||||
|
*/
|
||||||
|
registerButton(config) {
|
||||||
|
if (!config.id) {
|
||||||
|
console.error('[FurryPlace SDK] Button must have an id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registeredButtons.find(b => b.id === config.id)) {
|
||||||
|
console.error('[FurryPlace SDK] Button with id "' + config.id + '" already registered');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registeredButtons.push(config);
|
||||||
|
console.log('[FurryPlace SDK] Registered button:', config.id);
|
||||||
|
|
||||||
|
// If already initialized, inject immediately
|
||||||
|
if (isInitialized && injectionPoint) {
|
||||||
|
const button = createButton(config);
|
||||||
|
injectionPoint.appendChild(button);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a button
|
||||||
|
* @param {string} id - Button ID to remove
|
||||||
|
*/
|
||||||
|
unregisterButton(id) {
|
||||||
|
const index = registeredButtons.findIndex(b => b.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
registeredButtons.splice(index, 1);
|
||||||
|
|
||||||
|
// Remove from DOM
|
||||||
|
if (injectionPoint) {
|
||||||
|
const element = injectionPoint.querySelector('[data-furryplace-button="' + id + '"]');
|
||||||
|
if (element) {
|
||||||
|
element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FurryPlace SDK] Unregistered button:', id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of registered buttons
|
||||||
|
*/
|
||||||
|
getButtons() {
|
||||||
|
return registeredButtons.map(b => ({ id: b.id, title: b.title }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger SDK initialization
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
initialize();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current context
|
||||||
|
*/
|
||||||
|
getContext() {
|
||||||
|
return getContext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-load plugins from server
|
||||||
|
function autoLoadPlugins() {
|
||||||
|
console.log('[FurryPlace SDK] Auto-discovering plugins...');
|
||||||
|
|
||||||
|
fetch('/api/plugins')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.plugins || data.plugins.length === 0) {
|
||||||
|
console.log('[FurryPlace SDK] No plugins found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FurryPlace SDK] Found ' + data.plugins.length + ' plugin(s):', data.plugins.map(p => p.name));
|
||||||
|
|
||||||
|
// Load each plugin dynamically
|
||||||
|
data.plugins.forEach(plugin => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = plugin.path;
|
||||||
|
script.async = true;
|
||||||
|
script.onerror = () => {
|
||||||
|
console.error('[FurryPlace SDK] Failed to load plugin:', plugin.name);
|
||||||
|
};
|
||||||
|
script.onload = () => {
|
||||||
|
console.log('[FurryPlace SDK] Loaded plugin:', plugin.name);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.warn('[FurryPlace SDK] Failed to auto-discover plugins:', error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initialize();
|
||||||
|
autoLoadPlugins();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
initialize();
|
||||||
|
autoLoadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FurryPlace SDK] Loaded v' + SDK_VERSION);
|
||||||
|
})();
|
||||||
@@ -108,6 +108,9 @@
|
|||||||
href="./img/apple-touch-icon.png"
|
href="./img/apple-touch-icon.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="./site.webmanifest" />
|
<link rel="manifest" href="./site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- FurryPlace SDK for custom button extensions -->
|
||||||
|
<script src="/furryplace-sdk.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -95,6 +95,9 @@
|
|||||||
href="./img/apple-touch-icon.png"
|
href="./img/apple-touch-icon.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="./site.webmanifest" />
|
<link rel="manifest" href="./site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- FurryPlace SDK for custom button extensions -->
|
||||||
|
<script src="/furryplace-sdk.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# FurryPlace Plugins
|
||||||
|
|
||||||
|
This directory contains plugins that extend the FurryPlace UI using the FurryPlace SDK.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Create a plugin file** in this directory (e.g., `my-plugin.js`)
|
||||||
|
2. **Refresh the page** - Plugins are automatically discovered and loaded!
|
||||||
|
3. **Use the SDK** to register buttons or add other functionality
|
||||||
|
|
||||||
|
> **✨ Auto-Loading:** Plugins are automatically discovered from the `frontend-backup/plugins/` directory. Any `.js` file you add here will be automatically loaded when the page loads. No need to manually edit HTML files!
|
||||||
|
|
||||||
|
## Example Plugin
|
||||||
|
|
||||||
|
See [example-button.js](./example-button.js) for a complete example showing how to:
|
||||||
|
- Add custom buttons with icons
|
||||||
|
- Handle click events
|
||||||
|
- Use conditional rendering
|
||||||
|
- Apply custom styling
|
||||||
|
- Open external links
|
||||||
|
|
||||||
|
## FurryPlace SDK API
|
||||||
|
|
||||||
|
### `window.FurryPlaceSDK.registerButton(config)`
|
||||||
|
|
||||||
|
Register a custom button in the UI.
|
||||||
|
|
||||||
|
**Configuration Object:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: string, // Required: Unique identifier
|
||||||
|
title: string, // Required: Tooltip text
|
||||||
|
icon: string|HTMLElement, // Required: SVG string or DOM element
|
||||||
|
onClick: Function, // Required: (context, event) => {}
|
||||||
|
position: string, // Optional: 'top', 'bottom', 'before-leaderboard', 'after-leaderboard'
|
||||||
|
className: string, // Optional: Custom CSS classes (default: 'btn btn-square shadow-md')
|
||||||
|
wrapperClass: string, // Optional: Wrapper div CSS classes
|
||||||
|
condition: Function, // Optional: (context) => boolean
|
||||||
|
disabled: boolean // Optional: Whether button is disabled
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Positions:**
|
||||||
|
- `'top'` - First button in the container
|
||||||
|
- `'bottom'` - Last button in the container
|
||||||
|
- `'before-leaderboard'` - Before the leaderboard button
|
||||||
|
- `'after-leaderboard'` - After the leaderboard button (default)
|
||||||
|
|
||||||
|
**Context Object** (passed to `onClick` and `condition`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
sdk: {
|
||||||
|
version: string // SDK version
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
isLoggedIn: boolean // User login status (not yet implemented)
|
||||||
|
},
|
||||||
|
map: {} // Map context (not yet implemented)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other SDK Methods
|
||||||
|
|
||||||
|
- `FurryPlaceSDK.unregisterButton(id)` - Remove a registered button
|
||||||
|
- `FurryPlaceSDK.getButtons()` - Get list of all registered buttons
|
||||||
|
- `FurryPlaceSDK.getContext()` - Get current context object
|
||||||
|
- `FurryPlaceSDK.init()` - Manually initialize SDK (usually automatic)
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Wait for SDK to load
|
||||||
|
function waitForSDK(callback) {
|
||||||
|
if (window.FurryPlaceSDK) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => waitForSDK(callback), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForSDK(() => {
|
||||||
|
// Register a button
|
||||||
|
window.FurryPlaceSDK.registerButton({
|
||||||
|
id: 'my-custom-button',
|
||||||
|
title: 'My Button',
|
||||||
|
position: 'bottom',
|
||||||
|
icon: `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
|
||||||
|
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
onClick: (context) => {
|
||||||
|
alert('Button clicked!');
|
||||||
|
console.log('Context:', context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[My Plugin] Loaded!');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finding SVG Icons
|
||||||
|
|
||||||
|
You can find Material Design icons at:
|
||||||
|
- https://fonts.google.com/icons
|
||||||
|
|
||||||
|
Copy the SVG code and use it as the `icon` parameter.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Wrap your plugin in an IIFE** to avoid polluting the global scope
|
||||||
|
2. **Wait for the SDK** to be available before registering buttons
|
||||||
|
3. **Use unique IDs** to avoid conflicts with other plugins
|
||||||
|
4. **Test button positions** to find the best placement for your use case
|
||||||
|
5. **Check the console** for SDK loading messages and errors
|
||||||
|
|
||||||
|
## How Auto-Loading Works
|
||||||
|
|
||||||
|
The SDK automatically:
|
||||||
|
1. Calls `/api/plugins` to get a list of all `.js` files in the `plugins/` directory
|
||||||
|
2. Dynamically loads each plugin by creating `<script>` tags
|
||||||
|
3. Reports loading progress in the browser console
|
||||||
|
|
||||||
|
You can check which plugins are loaded by looking at the console:
|
||||||
|
```
|
||||||
|
[FurryPlace SDK] Auto-discovering plugins...
|
||||||
|
[FurryPlace SDK] Found 2 plugin(s): ['example-button.js', 'my-plugin.js']
|
||||||
|
[FurryPlace SDK] Loaded plugin: example-button.js
|
||||||
|
[FurryPlace SDK] Loaded plugin: my-plugin.js
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** If you need to disable a plugin, either delete it or rename it to something other than `.js` (like `.js.disabled`)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Button not appearing?**
|
||||||
|
- Check the browser console for errors
|
||||||
|
- Verify the SDK is loaded: `console.log(window.FurryPlaceSDK)`
|
||||||
|
- Ensure your button ID is unique
|
||||||
|
- Try different positions
|
||||||
|
|
||||||
|
**Button appearing in wrong place?**
|
||||||
|
- Try different `position` values
|
||||||
|
- Check if the button container has changed in the UI
|
||||||
|
|
||||||
|
**Icon not showing?**
|
||||||
|
- Verify your SVG has `viewBox` and `fill="currentColor"`
|
||||||
|
- Add `class="size-5"` to the SVG element
|
||||||
|
- Check for syntax errors in the SVG string
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Example FurryPlace Plugin - Custom Button
|
||||||
|
*
|
||||||
|
* This demonstrates how to use the FurryPlace SDK to add custom buttons
|
||||||
|
* to the UI. This plugin adds a "Help" button that opens a dialog.
|
||||||
|
*
|
||||||
|
* To use this plugin, add it to your HTML:
|
||||||
|
* <script src="/plugins/example-button.js"></script>
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Wait for SDK to be available
|
||||||
|
function waitForSDK(callback) {
|
||||||
|
if (window.FurryPlaceSDK) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => waitForSDK(callback), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 1: Simple button with SVG icon
|
||||||
|
function registerHelpButton() {
|
||||||
|
window.FurryPlaceSDK.registerButton({
|
||||||
|
id: 'example-help',
|
||||||
|
title: 'Help & Documentation',
|
||||||
|
position: 'after-leaderboard',
|
||||||
|
icon: `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
|
||||||
|
<path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
onClick: (context, event) => {
|
||||||
|
alert('Help button clicked!\n\nSDK Version: ' + context.sdk.version);
|
||||||
|
console.log('Context:', context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Button with custom styling
|
||||||
|
function registerDebugButton() {
|
||||||
|
window.FurryPlaceSDK.registerButton({
|
||||||
|
id: 'example-debug',
|
||||||
|
title: 'Debug Info',
|
||||||
|
position: 'bottom',
|
||||||
|
className: 'btn btn-square btn-accent shadow-md', // Custom styling
|
||||||
|
icon: `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
|
||||||
|
<path d="M440-280h80v-160h-80v160Zm40-240q17 0 28.5-11.5T520-560q0-17-11.5-28.5T480-600q-17 0-28.5 11.5T440-560q0 17 11.5 28.5T480-520Zm0 440q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
onClick: () => {
|
||||||
|
const info = {
|
||||||
|
buttons: window.FurryPlaceSDK.getButtons(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
viewport: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Debug Info:', info);
|
||||||
|
alert('Debug info logged to console');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Conditional button (only shows when user is logged in)
|
||||||
|
function registerConditionalButton() {
|
||||||
|
window.FurryPlaceSDK.registerButton({
|
||||||
|
id: 'example-conditional',
|
||||||
|
title: 'Premium Feature',
|
||||||
|
position: 'before-leaderboard',
|
||||||
|
icon: `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
|
||||||
|
<path d="m233-80 65-281L80-550l288-25 112-265 112 265 288 25-218 189 65 281-247-149L233-80Z"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
onClick: () => {
|
||||||
|
alert('This is a premium feature!');
|
||||||
|
},
|
||||||
|
// This condition is just an example - in a real plugin you'd check actual user state
|
||||||
|
condition: (context) => {
|
||||||
|
// For now, always show it (change this to check real user state)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Button that opens an external link
|
||||||
|
function registerDiscordButton() {
|
||||||
|
window.FurryPlaceSDK.registerButton({
|
||||||
|
id: 'example-discord',
|
||||||
|
title: 'Join our Discord',
|
||||||
|
position: 'top',
|
||||||
|
icon: `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
onClick: () => {
|
||||||
|
window.open('https://discord.gg/ZRC4DnP9Z2', '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize plugin
|
||||||
|
waitForSDK(() => {
|
||||||
|
console.log('[Example Plugin] Initializing...');
|
||||||
|
|
||||||
|
// Register all example buttons
|
||||||
|
registerHelpButton();
|
||||||
|
registerDebugButton();
|
||||||
|
registerConditionalButton();
|
||||||
|
registerDiscordButton();
|
||||||
|
|
||||||
|
console.log('[Example Plugin] Loaded successfully!');
|
||||||
|
console.log('[Example Plugin] Registered buttons:', window.FurryPlaceSDK.getButtons());
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to inject the FurryPlace SDK into the frontend HTML files
|
||||||
|
*
|
||||||
|
* This modifies the HTML files to include the SDK script tag
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const FRONTEND_DIR = path.join(__dirname, '..', 'frontend-backup');
|
||||||
|
const SDK_PATH = '/furryplace-sdk.js';
|
||||||
|
const HTML_FILES = ['index.html', 'admin.html', 'moderation.html'];
|
||||||
|
|
||||||
|
function injectSDK(htmlPath) {
|
||||||
|
console.log(`Processing ${htmlPath}...`);
|
||||||
|
|
||||||
|
let content = fs.readFileSync(htmlPath, 'utf8');
|
||||||
|
|
||||||
|
// Check if SDK is already injected
|
||||||
|
if (content.includes('furryplace-sdk.js')) {
|
||||||
|
console.log(` ✓ SDK already injected in ${path.basename(htmlPath)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the closing </head> tag and inject before it
|
||||||
|
const headCloseTag = '</head>';
|
||||||
|
const headCloseIndex = content.indexOf(headCloseTag);
|
||||||
|
|
||||||
|
if (headCloseIndex === -1) {
|
||||||
|
console.error(` ✗ Could not find </head> tag in ${path.basename(htmlPath)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the SDK script tag
|
||||||
|
const sdkScript = `\n\t\t<!-- FurryPlace SDK for custom button extensions -->\n\t\t<script src="${SDK_PATH}"></script>\n\t`;
|
||||||
|
|
||||||
|
// Inject the script
|
||||||
|
content = content.slice(0, headCloseIndex) + sdkScript + content.slice(headCloseIndex);
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
fs.writeFileSync(htmlPath, content, 'utf8');
|
||||||
|
console.log(` ✓ SDK injected into ${path.basename(htmlPath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log('FurryPlace SDK Injection Script\n');
|
||||||
|
|
||||||
|
// Check if SDK file exists
|
||||||
|
const sdkFilePath = path.join(FRONTEND_DIR, 'furryplace-sdk.js');
|
||||||
|
if (!fs.existsSync(sdkFilePath)) {
|
||||||
|
console.error(`Error: SDK file not found at ${sdkFilePath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`SDK file found: ${sdkFilePath}\n`);
|
||||||
|
|
||||||
|
// Process each HTML file
|
||||||
|
HTML_FILES.forEach(file => {
|
||||||
|
const htmlPath = path.join(FRONTEND_DIR, file);
|
||||||
|
|
||||||
|
if (!fs.existsSync(htmlPath)) {
|
||||||
|
console.warn(`Warning: ${file} not found, skipping...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectSDK(htmlPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n✓ SDK injection complete!');
|
||||||
|
console.log('\nYou can now create plugins by adding a script tag to your HTML:');
|
||||||
|
console.log(' <script src="/plugins/my-plugin.js"></script>');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -15,6 +15,7 @@ import pixel from "./routes/pixel.js";
|
|||||||
import store from "./routes/store.js";
|
import store from "./routes/store.js";
|
||||||
import { setupSiteContentRoutes } from "./routes/site-content.js";
|
import { setupSiteContentRoutes } from "./routes/site-content.js";
|
||||||
import { setupRulesRoutes } from "./routes/rules.js";
|
import { setupRulesRoutes } from "./routes/rules.js";
|
||||||
|
import { setupPluginRoutes } from "./routes/plugins.js";
|
||||||
import { addPrismaToRequest } from "./config/database.js";
|
import { addPrismaToRequest } from "./config/database.js";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
@@ -275,6 +276,7 @@ pixel(app);
|
|||||||
store(app);
|
store(app);
|
||||||
setupSiteContentRoutes(app);
|
setupSiteContentRoutes(app);
|
||||||
setupRulesRoutes(app);
|
setupRulesRoutes(app);
|
||||||
|
setupPluginRoutes(app);
|
||||||
|
|
||||||
const PORT = Number(process.env["PORT"]) || 3000;
|
const PORT = Number(process.env["PORT"]) || 3000;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { App } from '@tinyhttp/app';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
export function setupPluginRoutes(app: App) {
|
||||||
|
// Public endpoint - Get list of available plugins
|
||||||
|
app.get('/api/plugins', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const pluginsDir = path.join(__dirname, '..', '..', 'frontend-backup', 'plugins');
|
||||||
|
|
||||||
|
// Check if plugins directory exists
|
||||||
|
if (!fs.existsSync(pluginsDir)) {
|
||||||
|
return res.json({ plugins: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all .js files in plugins directory
|
||||||
|
const files = fs.readdirSync(pluginsDir);
|
||||||
|
const plugins = files
|
||||||
|
.filter(file => file.endsWith('.js') && file !== 'README.md')
|
||||||
|
.map(file => ({
|
||||||
|
name: file,
|
||||||
|
path: `/plugins/${file}`,
|
||||||
|
size: fs.statSync(path.join(pluginsDir, file)).size,
|
||||||
|
modified: fs.statSync(path.join(pluginsDir, file)).mtime
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return res.json({ plugins });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing plugins:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to list plugins', status: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user