Progress.

This commit is contained in:
retoor 2025-01-18 13:21:38 +01:00
parent 46a27405ae
commit a7446d1314
22 changed files with 1095 additions and 6 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
.vscode
.history
*.db*
*.png
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf
FROM python:3.10-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers git
#WKHTMLTOPDFNEEDS
RUN apk add --no-cache \
libstdc++ \
libx11 \
libxrender \
libxext \
libssl3 \
ca-certificates \
fontconfig \
freetype \
ttf-dejavu \
ttf-droid \
ttf-freefont \
ttf-liberation \
# more fonts
&& apk add --no-cache --virtual .build-deps \
msttcorefonts-installer \
# Install microsoft fonts
&& update-ms-fonts \
&& fc-cache -f \
# Clean up when done
&& rm -rf /tmp/* \
&& apk del .build-deps
COPY --from=wkhtmltopdf /bin/wkhtmltopdf /bin/wkhtmltopdf
COPY --from=wkhtmltopdf /bin/wkhtmltoimage /bin/wkhtmltoimage
COPY setup.cfg setup.cfg
COPY pyproject.toml pyproject.toml
COPY src src
RUN pip install --upgrade pip
RUN pip install -e .
EXPOSE 8081
CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]

View File

@ -1,6 +1,8 @@
PYTHON=./.venv/bin/python
PIP=./.venv/bin/pip
APP=./venv/bin/snek.serve
APP=./.venv/bin/snek.serve
GUNICORN=./.venv/bin/gunicorn
GUNICORN_WORKERS = 1
PORT = 8081
@ -9,5 +11,5 @@ install:
$(PIP) install -e .
run:
$(APP) --port=$(PORT)
$(GUNICORN) -w $(GUNICORN_WORKERS) -k aiohttp.worker.GunicornWebWorker snek.gunicorn:app --bind 0.0.0.0:$(PORT) --reload

0
cache/crc321300331366.cache vendored Normal file
View File

0
cache/crc322507170282.cache vendored Normal file
View File

12
compose.yml Normal file
View File

@ -0,0 +1,12 @@
services:
snek:
build: .
ports:
- "8081:8081"
volumes:
- ./:/code
develop:
watch:
- action: sync
path: .
target: /code

View File

@ -15,6 +15,10 @@ package_dir =
python_requires = >=3.7
install_requires =
app @ git+https://retoor.molodetz.nl/retoor/app
beautifulsoup4
gunicorn
imgkit
wkhtmltopdf
[options.packages.find]
where = src

View File

@ -1,20 +1,60 @@
from app.app import Application as BaseApplication
from snek.forms import RegisterForm
from aiohttp import web
import aiohttp
import pathlib
from snek import http
from snek.middleware import cors_allow_middleware,cors_middleware
class Application(BaseApplication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
middlewares = [
cors_middleware,
web.normalize_path_middleware(merge_slashes=True)
]
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
super().__init__(middlewares=middlewares, template_path=self.template_path,*args, **kwargs)
self.router.add_static("/",pathlib.Path(__file__).parent.joinpath("static"),name="static",show_index=True)
self.router.add_get("/register", self.handle_register)
self.router.add_get("/login", self.handle_login)
self.router.add_get("/test", self.handle_test)
self.router.add_post("/register", self.handle_register)
self.router.add_get("/http-get",self.handle_http_get)
self.router.add_get("/http-photo",self.handle_http_photo)
async def handle_test(self,request):
return await self.render_template("test.html",request,context={"name":"retoor"})
async def handle_http_get(self, request:web.Request):
url = request.query.get("url")
content = await http.get(url)
return web.Response(body=content)
async def handle_http_photo(self, request):
url = request.query.get("url")
path = await http.create_site_photo(url)
return web.Response(body=path.read_bytes(),headers={
"Content-Type": "image/png"
})
async def handle_login(self, request):
if request.method == "GET":
return await self.render_template("login.html", request) #web.json_response({"form": RegisterForm().to_json()})
elif request.method == "POST":
return await self.render_template("login.html", request) #web.json_response({"form": RegisterForm().to_json()})
async def handle_register(self, request):
if request.method == "GET":
return web.json_response({"form": RegisterForm().to_json()})
return await self.render_template("register.html", request) #web.json_response({"form": RegisterForm().to_json()})
elif request.method == "POST":
return self.render("register.html")
if __name__ == '__main__':
app = Application()
if __name__ == '__main__':
web.run_app(app,port=8081,host="0.0.0.0")

3
src/snek/gunicorn.py Normal file
View File

@ -0,0 +1,3 @@
from snek.app import app
application = app

83
src/snek/http.py Normal file
View File

@ -0,0 +1,83 @@
from aiohttp import web
import aiohttp
from app.cache import time_cache_async
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import pathlib
import uuid
import imgkit
import asyncio
import zlib
import io
async def crc32(data):
try:
data = data.encode()
except:
pass
result = "crc32" + str(zlib.crc32(data))
return result
async def get_file(name,suffix=".cache"):
name = await crc32(name)
path = pathlib.Path(".").joinpath("cache")
if not path.exists():
path.mkdir(parents=True,exist_ok=True)
path = path.joinpath(name + suffix)
return path
async def public_touch(name=None):
path = pathlib.Path(".").joinpath(str(uuid.uuid4())+name)
path.open("wb").close()
return path
async def create_site_photo(url):
loop = asyncio.get_event_loop()
if not url.startswith("https"):
url = "https://" + url
output_path = await get_file("site-screenshot-" + url,".png")
if output_path.exists():
return output_path
output_path.touch()
def make_photo():
imgkit.from_url(url, output_path.absolute())
return output_path
return await loop.run_in_executor(None,make_photo)
async def repair_links(base_url, html_content):
soup = BeautifulSoup(html_content, "html.parser")
for tag in soup.find_all(['a', 'img', 'link']):
if tag.has_attr('href') and not tag['href'].startswith("http"): # For <a> and <link> tags
tag['href'] = urljoin(base_url, tag['href'])
if tag.has_attr('src') and not tag['src'].startswith("http"): # For <img> tags
tag['src'] = urljoin(base_url, tag['src'])
print("Fixed: ",tag['src'])
return soup.prettify()
async def is_html_content(content: bytes):
try:
content = content.decode(errors='ignore')
except:
pass
marks = ['<html','<img','<p','<span','<div']
try:
content = content.lower()
for mark in marks:
if mark in content:
return True
except Exception as ex:
print(ex)
return False
@time_cache_async(120)
async def get(url):
async with aiohttp.ClientSession() as session:
response = await session.get(url)
content = await response.text()
if await is_html_content(content):
content = (await repair_links(url,content)).encode()
return content

32
src/snek/middleware.py Normal file
View File

@ -0,0 +1,32 @@
from aiohttp import web
@web.middleware
async def no_cors_middleware(request, handler):
response = await handler(request)
response.headers.pop("Access-Control-Allow-Origin", None)
return response
@web.middleware
async def cors_allow_middleware(request ,handler):
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "*"
return response
@web.middleware
async def cors_middleware(request, handler):
# Handle preflight (OPTIONS) requests
if request.method == "OPTIONS":
response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"
return response
# Handle actual requests
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"
return response

77
src/snek/static/app.js Normal file
View File

@ -0,0 +1,77 @@
class Message {
uid = null
author = null
avatar = null
text = null
time = null
constructor(uid,avatar,author,text,time){
this.uid = uid
this.avatar = avatar
this.author = author
this.text = text
this.time = time
}
get links() {
if(!this.text)
return []
let result = []
for(let part in this.text.split(/[,; ]/)){
if(part.startsWith("http") || part.startsWith("www.") || part.indexOf(".com") || part.indexOf(".net") || part.indexOf(".io") || part.indexOf(".nl")){
result.push(part)
}
}
return result
}
get mentions() {
if(!this.text)
return []
let result = []
for(let part in this.text.split(/[,; ]/)){
if(part.startsWith("@")){
result.push(part)
}
}
return result
}
}
class Messages {
}
class Room {
name = null
messages = []
constructor(name){
this.name = name
}
setMessages(list){
}
}
class App {
rooms = []
constructor() {
this.rooms.push(new Room("General"))
}
}

View File

@ -0,0 +1,9 @@
.html-frame {
width: 100px;
height: 50px;
position: relative;
overflow: hidden;
clip-path: inset(0px 0px 50px 100px); /* Crop content */
border: 1px solid black;
}

View File

@ -0,0 +1,39 @@
class HTMLFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement('div');
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.container.classList.add("html_frame")
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
console.info(fullUrl)
this.fetchAndDisplayHtml(fullUrl.toString());
} else {
this.container.textContent = "No URL provided!";
}
}
async fetchAndDisplayHtml(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}
const html = await response.text();
this.container.innerHTML = html; // Insert the fetched HTML into the container
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
// Define the custom element
customElements.define('html-frame', HTMLFrame);

View File

@ -0,0 +1,172 @@
/* General Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Body Styling */
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header Navigation */
header {
background-color: #0f0f0f;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header .logo {
color: #fff;
font-size: 1.5em;
font-weight: bold;
}
header nav a {
color: #aaa;
text-decoration: none;
margin-left: 15px;
font-size: 1em;
transition: color 0.3s;
}
header nav a:hover {
color: #fff;
}
/* Main Layout */
main {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 250px;
background-color: #121212;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #333;
}
.sidebar h2 {
color: #f05a28;
font-size: 1.2em;
margin-bottom: 20px;
}
.sidebar ul {
list-style: none;
}
.sidebar ul li {
margin-bottom: 15px;
}
.sidebar ul li a {
color: #ccc;
text-decoration: none;
font-size: 1em;
transition: color 0.3s;
}
.sidebar ul li a:hover {
color: #fff;
}
/* Chat Area */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: #1a1a1a;
}
.chat-header {
padding: 10px 20px;
background-color: #0f0f0f;
border-bottom: 1px solid #333;
}
.chat-header h2 {
font-size: 1.2em;
color: #fff;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #1a1a1a;
}
.chat-messages .message {
margin-bottom: 15px;
}
.chat-messages .message .author {
font-weight: bold;
color: #f05a28;
}
.chat-messages .message .content {
margin-left: 10px;
color: #e6e6e6;
}
/* Input Area */
.chat-input {
padding: 15px;
background-color: #121212;
display: flex;
align-items: center;
border-top: 1px solid #333;
}
.chat-input textarea {
flex: 1;
background-color: #1a1a1a;
color: #fff;
border: none;
padding: 10px;
border-radius: 5px;
resize: none;
}
.chat-input button {
background-color: #f05a28;
color: white;
border: none;
padding: 10px 15px;
margin-left: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s;
}
.chat-input button:hover {
background-color: #e04924;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.sidebar {
display: none;
}
.chat-area {
flex: 1;
}
}

View File

@ -0,0 +1,95 @@
/* General Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Body Styling */
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Registration Form Container */
.registration-container {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
}
/* Form Heading */
.registration-container h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
}
/* Input Fields */
.registration-container input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #333;
border-radius: 5px;
background-color: #1a1a1a;
color: #e6e6e6;
font-size: 1em;
}
/* Submit Button */
.registration-container button {
width: 100%;
padding: 10px;
background-color: #f05a28;
border: none;
border-radius: 5px;
color: white;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
.registration-container button:hover {
background-color: #e04924;
}
/* Links */
.registration-container a {
color: #f05a28;
text-decoration: none;
display: block;
margin-top: 15px;
font-size: 0.9em;
transition: color 0.3s;
}
.registration-container a:hover {
color: #e04924;
}
/* Error Message Styling */
.error {
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
}
/* Responsive Design */
@media (max-width: 500px) {
.registration-container {
width: 90%;
}
}

203
src/snek/static/styles.css Normal file
View File

@ -0,0 +1,203 @@
/* General Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Body Styling */
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header Navigation */
header {
background-color: #0f0f0f;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header .logo {
color: #fff;
font-size: 1.5em;
font-weight: bold;
}
header nav a {
color: #aaa;
text-decoration: none;
margin-left: 15px;
font-size: 1em;
transition: color 0.3s;
}
header nav a:hover {
color: #fff;
}
/* Main Layout */
main {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 250px;
background-color: #121212;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #333;
}
.sidebar h2 {
color: #f05a28;
font-size: 1.2em;
margin-bottom: 20px;
}
.sidebar ul {
list-style: none;
}
.sidebar ul li {
margin-bottom: 15px;
}
.sidebar ul li a {
color: #ccc;
text-decoration: none;
font-size: 1em;
transition: color 0.3s;
}
.sidebar ul li a:hover {
color: #fff;
}
/* Chat Area */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: #1a1a1a;
}
.chat-header {
padding: 10px 20px;
background-color: #0f0f0f;
border-bottom: 1px solid #333;
}
.chat-header h2 {
font-size: 1.2em;
color: #fff;
}
/* Chat Messages */
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #1a1a1a;
}
.chat-messages .message {
display: flex;
align-items: flex-start;
margin-bottom: 15px;
padding: 10px;
background: #222;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
.chat-messages .message .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f05a28;
color: #fff;
font-size: 1em;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.chat-messages .message .message-content {
flex: 1;
}
.chat-messages .message .message-content .author {
font-weight: bold;
color: #f05a28;
margin-bottom: 3px;
}
.chat-messages .message .message-content .text {
margin-bottom: 5px;
color: #e6e6e6;
}
.chat-messages .message .message-content .time {
font-size: 0.8em;
color: #aaa;
}
/* Input Area */
.chat-input {
padding: 15px;
background-color: #121212;
display: flex;
align-items: center;
border-top: 1px solid #333;
}
.chat-input textarea {
flex: 1;
background-color: #1a1a1a;
color: white;
border: none;
padding: 10px;
border-radius: 5px;
resize: none;
}
.chat-input button {
background-color: #f05a28;
color: white;
border: none;
padding: 10px 15px;
margin-left: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s;
}
.chat-input button:hover {
background-color: #e04924;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.sidebar {
display: none;
}
.chat-area {
flex: 1;
}
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<link rel="stylesheet" href="register.css">
</head>
<body>
<div class="registration-container">
<h1>Login</h1>
<form>
<input type="text" name="username" placeholder="Username or password" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Create Account</button>
<a href="/register">Not having an account yet? Register here.</a>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dark Themed Chat Application</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<div class="logo">Molodetz Chat</div>
<nav>
<a href="#">Home</a>
<a href="#">Rooms</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</nav>
</header>
<main>
<aside class="sidebar">
<h2>Chat Rooms</h2>
<ul>
<li><a href="#">General</a></li>
<li><a href="#">Development</a></li>
<li><a href="#">Support</a></li>
<li><a href="#">Random</a></li>
</ul>
</aside>
<section class="chat-area">
<div class="chat-header">
<h2>General</h2>
</div>
<div class="chat-messages">
<div class="message">
<span class="author">Alice:</span>
<span class="content">Hello, everyone!</span>
</div>
<div class="message">
<span class="author">Bob:</span>
<span class="content">Hi Alice! How are you?</span>
</div>
</div>
<div class="chat-input">
<textarea placeholder="Type a message..." rows="2"></textarea>
<button>Send</button>
</div>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<link rel="stylesheet" href="register.css">
</head>
<body>
<div class="registration-container">
<h1>Register</h1>
<form>
<input type="text" name="username" placeholder="Username" required>
<input type="email" name="email" placeholder="Email Address" required>
<input type="password" name="password" placeholder="Password" required>
<input type="password" name="confirm_password" placeholder="Confirm Password" required>
<button type="submit">Create Account</button>
<a href="#">Already have an account? Login here.</a>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dark Themed Chat Application</title>
<link rel="stylesheet" href="styles.css">
<script src="/html_frame.js"></script>
<script src="/html_frame.css"></script>
</head>
<body>
<header>
<div class="logo">Molodetz Chat</div>
<nav>
<a href="#">Home</a>
<a href="#">Rooms</a>
<a href="#">Settings</a>
<a href="#">Logout</a>
</nav>
</header>
<main>
<aside class="sidebar">
<h2>Chat Rooms</h2>
<ul>
<li><a href="#">General</a></li>
<li><a href="#">Development</a></li>
<li><a href="#">Support</a></li>
<li><a href="#">Random</a></li>
</ul>
</aside>
<section class="chat-area">
<div class="chat-header">
<h2>General</h2>
</div>
<div class="chat-messages">
<div class="message">
<div class="avatar">A</div>
<div class="message-content">
<div class="author">Alice</div>
<div class="text">Hello, everyone!</div>
<div class="time">10:45 AM</div>
</div>
</div>
<html-frame class="html-frame" url="/register"></html-frame>
<div class="message">
<div class="avatar">B</div>
<div class="message-content">
<div class="author">Bob</div>
<div class="text">Hi Alice! How are you?</div>
<div class="time">10:46 AM</div>
</div>
</div>
</div>
<div class="chat-input">
<textarea placeholder="Type a message..." rows="2"></textarea>
<button>Send</button>
</div>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Form Component</title>
<style>
.form-container {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
.form-field {
margin-bottom: 15px;
}
.form-field label {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
.form-field input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 3px;
}
.form-field .error {
color: red;
font-size: 0.9em;
margin-top: 5px;
}
</style>
</head>
<body>
<!-- Use the custom form component -->
<dynamic-form></dynamic-form>
<script>
class DynamicForm extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// Sample data for the form
const formData = {
form: {
uid: { required: true, value: "e13ad3b7-20b2-4c1a-b74e-b8c7d1abd107", html_type: "text", place_holder: "UID", is_valid: true },
created_at: { required: true, value: "2025-01-17 21:21:27.561769+00:00", html_type: "text", place_holder: "Created At", is_valid: true },
updated_at: { required: false, value: null, html_type: "text", place_holder: "Updated At", is_valid: true },
deleted_at: { required: false, value: null, html_type: "text", place_holder: "Deleted At", is_valid: true },
email: { required: true, value: null, html_type: "email", place_holder: "Email address", errors: ["Field is required."], is_valid: false },
password: { required: true, value: null, html_type: "password", place_holder: "Password", errors: ["Field is required."], is_valid: false },
username: { required: true, value: null, html_type: "text", place_holder: "Username", errors: ["Field is required."], is_valid: false }
}
};
// Render the form
this.render(formData);
}
render(data) {
const form = data.form;
const container = document.createElement('div');
container.className = 'form-container';
// Create a form element
const formElement = document.createElement('form');
// Generate form fields from the data
Object.entries(form).forEach(([fieldName, fieldData]) => {
const fieldContainer = document.createElement('div');
fieldContainer.className = 'form-field';
// Add label
const label = document.createElement('label');
label.textContent = fieldName.replace(/_/g, ' ').toUpperCase();
label.htmlFor = fieldName;
fieldContainer.appendChild(label);
// Add input field
const input = document.createElement('input');
input.type = fieldData.html_type || 'text';
input.name = fieldName;
input.value = fieldData.value || '';
input.placeholder = fieldData.place_holder || '';
input.required = fieldData.required || false;
// Append input to the container
fieldContainer.appendChild(input);
// Display validation errors
if (fieldData.errors && fieldData.errors.length > 0) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = fieldData.errors.join(', ');
fieldContainer.appendChild(errorDiv);
}
// Append field to the form
formElement.appendChild(fieldContainer);
});
// Add the form to the container
container.appendChild(formElement);
// Append the container to the shadow DOM
this.shadowRoot.appendChild(container);
}
}
// Define the custom element
customElements.define('dynamic-form', DynamicForm);
</script>
</body>
</html>