frontend
This commit is contained in:
parent
2d198c838f
commit
4a4f7ef508
12 changed files with 3076 additions and 0 deletions
2
frontend/.gitignore
vendored
Normal file
2
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
24
frontend/README.md
Normal file
24
frontend/README.md
Normal 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
19
frontend/dist/index.html
vendored
Normal 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
33
frontend/eslint.config.js
Normal 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
18
frontend/index.html
Normal 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
2719
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
frontend/package.json
Normal file
34
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
84
frontend/src/CommentForm.jsx
Normal file
84
frontend/src/CommentForm.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
41
frontend/src/CommentList.jsx
Normal file
41
frontend/src/CommentList.jsx
Normal 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
24
frontend/src/main.jsx
Normal 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
71
frontend/src/style.css
Normal 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
7
frontend/vite.config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue