meow
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
*.log
|
||||
@@ -1 +1,38 @@
|
||||
# linkweb
|
||||
# Toothless' Bot Home
|
||||
|
||||
React + Vite + Tailwind link home running on port `3020`.
|
||||
|
||||
## Local
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
npm run serve
|
||||
```
|
||||
|
||||
Open `http://localhost:3020`.
|
||||
|
||||
## PM2
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
pm2 start ecosystem.config.cjs
|
||||
```
|
||||
|
||||
The app name is `linkweb`. The GitHub webhook receiver is:
|
||||
|
||||
```text
|
||||
POST /webhook/github
|
||||
```
|
||||
|
||||
Set `WEBHOOK_SECRET` in `ecosystem.config.cjs` if you want GitHub `x-hub-signature-256` verification:
|
||||
|
||||
```js
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: '3020',
|
||||
PM2_APP_NAME: 'linkweb',
|
||||
WEBHOOK_PATH: '/webhook/github',
|
||||
WEBHOOK_SECRET: 'your-secret',
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'linkweb',
|
||||
script: 'server.js',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: '3020',
|
||||
PM2_APP_NAME: 'linkweb',
|
||||
WEBHOOK_PATH: '/webhook/github',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1e0618" />
|
||||
<title>Toothless' Bot Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3059
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "toothless-bot-home",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0 --port 3020",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 0.0.0.0 --port 3020",
|
||||
"serve": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"express": "^5.1.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { execFile } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import express from 'express';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
const port = Number(process.env.PORT || 3020);
|
||||
const webhookPath = process.env.WEBHOOK_PATH || '/webhook/github';
|
||||
const webhookSecret = process.env.WEBHOOK_SECRET || '';
|
||||
const pm2AppName = process.env.PM2_APP_NAME || 'linkweb';
|
||||
const pm2Command = process.env.PM2_BIN || (process.platform === 'win32' ? 'pm2.cmd' : 'pm2');
|
||||
|
||||
function verifyGitHubSignature(req) {
|
||||
if (!webhookSecret) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const signature = req.get('x-hub-signature-256');
|
||||
if (!signature?.startsWith('sha256=')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const digest = `sha256=${crypto
|
||||
.createHmac('sha256', webhookSecret)
|
||||
.update(req.body)
|
||||
.digest('hex')}`;
|
||||
|
||||
const received = Buffer.from(signature);
|
||||
const expected = Buffer.from(digest);
|
||||
|
||||
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
|
||||
}
|
||||
|
||||
app.post(webhookPath, express.raw({ type: '*/*' }), (req, res) => {
|
||||
if (!verifyGitHubSignature(req)) {
|
||||
res.status(401).json({ ok: false, error: 'Invalid webhook signature' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(202).json({ ok: true, restarting: pm2AppName });
|
||||
|
||||
execFile(pm2Command, ['restart', pm2AppName], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('PM2 restart failed:', error.message);
|
||||
if (stderr) {
|
||||
console.error(stderr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/healthz', (_req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'dist')));
|
||||
|
||||
app.use((_req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Toothless' Bot Home listening on port ${port}`);
|
||||
});
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Bot, ExternalLink, MessageCircle, Sparkles } from 'lucide-react';
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'TSSBot',
|
||||
href: 'https://tss.pawjob.us/',
|
||||
description: 'TSSBot - War Thunder Tournaments Scheduling Service',
|
||||
accent: 'bg-cerulean-500',
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
name: 'SREBot',
|
||||
href: 'https://sre.pawjob.us/',
|
||||
description: 'SREBot - The best squadron battles bot',
|
||||
accent: 'bg-yale-blue-500',
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
name: 'Spectra',
|
||||
href: 'https://spectra.artvv.dev/',
|
||||
description: 'Spectra - WE ARE POWERED BY THEM',
|
||||
accent: 'bg-prussian-blue-400',
|
||||
icon: Sparkles,
|
||||
},
|
||||
{
|
||||
name: 'Support Discord',
|
||||
href: 'https://discord.gg/VXPcQaJerE',
|
||||
description: 'For support, questions and contact.',
|
||||
accent: 'bg-white-400',
|
||||
icon: MessageCircle,
|
||||
},
|
||||
];
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main className="min-h-screen bg-deep-navy-950 text-yale-blue-50">
|
||||
<section className="mx-auto flex min-h-screen w-full max-w-5xl flex-col justify-center px-5 py-10 sm:px-8">
|
||||
<div className="mb-8 max-w-3xl border-b border-prussian-blue-700 pb-6">
|
||||
<p className="mb-3 text-sm font-semibold uppercase tracking-[0.16em] text-cerulean-300">
|
||||
Gateway
|
||||
</p>
|
||||
<h1 className="text-4xl font-bold leading-tight text-white sm:text-5xl">
|
||||
Toothless' Bot Home.
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-base leading-7 text-yale-blue-100 sm:text-lg">
|
||||
Our projects and affiliates.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{links.map((link) => {
|
||||
const Icon = link.icon;
|
||||
|
||||
return (
|
||||
<a
|
||||
className="group relative min-h-36 rounded-lg border border-prussian-blue-700 bg-prussian-blue-900 p-5 transition duration-150 hover:border-cerulean-400 hover:bg-prussian-blue-800 focus:outline-none focus:ring-2 focus:ring-cerulean-300"
|
||||
href={link.href}
|
||||
key={link.name}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className={`absolute bottom-0 left-0 top-0 w-1 rounded-l-lg ${link.accent}`} />
|
||||
<span className="flex h-full flex-col justify-between gap-8">
|
||||
<span className="flex items-start justify-between gap-4">
|
||||
<span className="grid size-11 place-items-center rounded-md border border-prussian-blue-600 bg-deep-navy-900 text-cerulean-200">
|
||||
<Icon aria-hidden="true" className="size-6" strokeWidth={2.2} />
|
||||
</span>
|
||||
<ExternalLink
|
||||
aria-hidden="true"
|
||||
className="size-5 text-yale-blue-200/70 transition group-hover:text-cerulean-200"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span className="block text-2xl font-extrabold text-white">{link.name}</span>
|
||||
<span className="mt-2 block text-sm leading-6 text-yale-blue-100/82">
|
||||
{link.description}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './styles.css';
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-prussian-blue-50: #ebeefa;
|
||||
--color-prussian-blue-100: #d6ddf5;
|
||||
--color-prussian-blue-200: #adbceb;
|
||||
--color-prussian-blue-300: #859ae0;
|
||||
--color-prussian-blue-400: #5c78d6;
|
||||
--color-prussian-blue-500: #3357cc;
|
||||
--color-prussian-blue-600: #2945a3;
|
||||
--color-prussian-blue-700: #1f347a;
|
||||
--color-prussian-blue-800: #142352;
|
||||
--color-prussian-blue-900: #0a1129;
|
||||
--color-prussian-blue-950: #070c1d;
|
||||
|
||||
--color-deep-navy-50: #e5efff;
|
||||
--color-deep-navy-100: #ccdfff;
|
||||
--color-deep-navy-200: #99beff;
|
||||
--color-deep-navy-300: #669eff;
|
||||
--color-deep-navy-400: #337eff;
|
||||
--color-deep-navy-500: #005eff;
|
||||
--color-deep-navy-600: #004bcc;
|
||||
--color-deep-navy-700: #003899;
|
||||
--color-deep-navy-800: #002566;
|
||||
--color-deep-navy-900: #001333;
|
||||
--color-deep-navy-950: #000d24;
|
||||
|
||||
--color-yale-blue-50: #e6f3fe;
|
||||
--color-yale-blue-100: #cde6fe;
|
||||
--color-yale-blue-200: #9ccefc;
|
||||
--color-yale-blue-300: #6ab5fb;
|
||||
--color-yale-blue-400: #389cfa;
|
||||
--color-yale-blue-500: #0684f9;
|
||||
--color-yale-blue-600: #0569c7;
|
||||
--color-yale-blue-700: #044f95;
|
||||
--color-yale-blue-800: #033563;
|
||||
--color-yale-blue-900: #011a32;
|
||||
--color-yale-blue-950: #011223;
|
||||
|
||||
--color-cerulean-50: #e8f8fc;
|
||||
--color-cerulean-100: #d1f1fa;
|
||||
--color-cerulean-200: #a3e3f5;
|
||||
--color-cerulean-300: #75d5f0;
|
||||
--color-cerulean-400: #47c7eb;
|
||||
--color-cerulean-500: #19b9e6;
|
||||
--color-cerulean-600: #1494b8;
|
||||
--color-cerulean-700: #0f6f8a;
|
||||
--color-cerulean-800: #0a4a5c;
|
||||
--color-cerulean-900: #05252e;
|
||||
--color-cerulean-950: #041a20;
|
||||
|
||||
--color-white-50: #faf0eb;
|
||||
--color-white-100: #f5e0d6;
|
||||
--color-white-200: #ebc2ad;
|
||||
--color-white-300: #e0a385;
|
||||
--color-white-400: #d6855c;
|
||||
--color-white-500: #cc6633;
|
||||
--color-white-600: #a35229;
|
||||
--color-white-700: #7a3d1f;
|
||||
--color-white-800: #522914;
|
||||
--color-white-900: #29140a;
|
||||
--color-white-950: #1d0e07;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
background: #000d24;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
Reference in New Issue
Block a user