This commit is contained in:
me 2025-03-13 23:36:54 +02:00
parent 2d198c838f
commit 4a4f7ef508
12 changed files with 3076 additions and 0 deletions

2
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

24
frontend/README.md Normal file
View File

@ -0,0 +1,24 @@
# Universal Comments System
## Build
Build and run on port 3002.
```sh
(npm run build; cd dist/assets; mv *.js index.js; mv *.css index.css; warp --port 3002)
```
## Use
In your HTML, add the following:
```html
<div id="ucs-comments"
data-element="ucs-comments"
data-ucs_url="[BACKEND_URL]"
data-site="[WEBSITE]"
data-path="[PAGE_PATH]"
></div>
<script type="module" src="[FRONTEND_URL]/index.js"></script>
<link rel="stylesheet" type="text/css" href="[FRONTEND_URL]/index.css">
```

19
frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ucs</title>
<script type="module" crossorigin src="/assets/index-DxfpMQtn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-TEs_pY0h.css">
</head>
<body>
<div id="ucs-comments"
data-element="ucs-comments"
data-ucs_url="http://localhost:8080"
data-site="alloca.space"
data-path="blog.html"
></div>
</body>
</html>

33
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

18
frontend/index.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ucs</title>
</head>
<body>
<div id="ucs-comments"
data-element="ucs-comments"
data-ucs_url="http://localhost:8080"
data-site="alloca.space"
data-path="blog.html"
></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2719
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
frontend/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "ucs-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"description": "Frontend for the Universal Comment System",
"author": "alloca",
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "https://git.alloca.space/me/ucs.git"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"vite": "^6.2.0"
}
}

View File

@ -0,0 +1,84 @@
import { useState } from "react";
import './style.css'
function isValidHttpUrl(string) {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
export default function CommentForm({ url, site, path }) {
const [error, setError] = useState(null);
const handleSubmit = e => {
console.log(e);
e.preventDefault();
let form = {
name: e.target.elements.name.value,
website: e.target.elements.user_website.value,
message: e.target.elements.message.value,
token: e.target.elements.token.value
};
if (form.name.length === 0) {
setError("הזינו שם.");
return;
}
if (form.message.length === 0) {
setError("תגובה ריקה.");
return;
}
if (form.token.length === 0) {
setError("תשובה לשאלת סינון חסרה.");
return;
}
if (form.website.length > 0 && !isValidHttpUrl(form.website)) {
setError("אתר לא תקין. אולי שכחתם לציין את הפרוטוקול?");
return;
}
fetch(url + '/url/' + site + '/' + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(form)
})
.then(response => {
console.log(response);
if (response.ok) {
window.location.reload();
} else {
response.json().then((err) => { console.log(err); setError(err); });
}
})
.catch(error => {
console.log(error);
});
};
return (
<form onSubmit={handleSubmit} className="new-comment">
<h4>כתיבת תגובה</h4>
<div>
<textarea className="new-comment-message" name="message" minLength="1" maxLength="1000" required="" placeholder="התגובה שלך" />
</div>
<div className="new-comment-user">
<input className="new-comment-username" type="text" name="name" placeholder="שם" minLength="1" required="" />
<input className="new-comment-user-website" type="url" name="user_website" placeholder="אתר" />
</div>
<div className="new-comment-div">
<input className="new-comment-token" type="text" name="token" required="" minLength="1" placeholder="שאלת סינון" />
<button className="new-comment-submit" type="submit" title="Add a new submission">הוספה</button>
</div>
<div className="error"><p>{error}</p></div>
</form>
)
}

View File

@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
import './style.css'
const reply = details => {
return (
<li className="comment-li" key={details.id}>
<details className="comment-details" open>
<summary className="comment-user">
{ details.user_website ? <a href={details.user_website}>{details.user}</a> : details.user }
</summary>
<p dangerouslySetInnerHTML={{ __html: details.message }} />
</details>
</li>
)
};
// <p>{details.message}</p>
export default function CommentList({ url, site, path }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch(url + '/url/' + site + '/' + path)
.then(response => { console.log(response); return response.json() })
.then(data => setComments(data))
.catch(error => {
console.log(error);
});
}, []);
return (
<div className="comments-list">
<h3>
{ comments.length == 1 ? "תגובה אחת" : comments.length + " תגובות" }
</h3>
<div>
<ul className="comments">
{comments.map(comment => reply(comment))}
</ul>
</div>
</div>
)
}

24
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,24 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import CommentList from './CommentList.jsx'
import CommentForm from './CommentForm.jsx'
function UcsComments(settings) {
return (
createRoot(document.getElementById(settings.element)).render(
<StrictMode>
<div className="ucs-comments">
<CommentList url={settings.ucs_url} site={settings.site} path={settings.path} />
<CommentForm url={settings.ucs_url} site={settings.site} path={settings.path} />
</div>
</StrictMode>,
)
)
};
const settings = () => {
let element = document.getElementById('ucs-comments');
return element.dataset;
};
UcsComments(settings());

71
frontend/src/style.css Normal file
View File

@ -0,0 +1,71 @@
.ucs-comments {
border-top: 1px solid;
margin-top: 50px;
padding-top: 40px;
direction: rtl;
}
.comments-list {
box-sizing: border-box;
margin: auto;
padding-bottom: 20px;
}
.comments {
padding: 0px;
}
.comment-li {
list-style: none;
}
.comment-details {
border-radius: 5px;
}
.comment-details summary {
padding: 5px 5px;
cursor: pointer;
}
.comment-details summary::marker {
padding: 5px;
}
.comment-details p {
margin: 5px;
padding: 5px 10px;
}
.new-comment {
box-sizing: border-box;
margin: auto;
}
.new-comment input, textarea {
padding: 5px;
}
.new-comment input::placeholder, textarea::placeholder {
}
.new-comment-user { width: 100%; display: flex; align-items: stretch; gap: 5px; }
.new-comment-username, .new-comment-user-website {
flex-basis: 100%;
box-sizing: border-box;
flex-grow: 1;
}
.new-comment-user-website {
direction: ltr;
text-align: left;
}
.new-comment-user-website::placeholder {
direction: rtl;
text-align: right;
}
.new-comment div { width: 100%; margin-bottom: 10px; box-sizing: border-box; }
.new-comment-message {
max-width: 100%;
min-width: 100%;
min-height: 100px;
margin: auto;
border-radius: 5px;
box-sizing: border-box;
}
.new-comment-div { width: 100%; display: flex; align-items: stretch; gap: 5px; }
.new-comment-token, .new-comment-user-submit {
flex-basis: 100%;
box-sizing: border-box;
flex-grow: 1;
}
.error { color: #ff7777; }

7
frontend/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})