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 .vscode
.history .history
*.db* *.db*
*.png
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __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 PYTHON=./.venv/bin/python
PIP=./.venv/bin/pip PIP=./.venv/bin/pip
APP=./venv/bin/snek.serve APP=./.venv/bin/snek.serve
GUNICORN=./.venv/bin/gunicorn
GUNICORN_WORKERS = 1
PORT = 8081 PORT = 8081
@ -9,5 +11,5 @@ install:
$(PIP) install -e . $(PIP) install -e .
run: 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 python_requires = >=3.7
install_requires = install_requires =
app @ git+https://retoor.molodetz.nl/retoor/app app @ git+https://retoor.molodetz.nl/retoor/app
beautifulsoup4
gunicorn
imgkit
wkhtmltopdf
[options.packages.find] [options.packages.find]
where = src where = src

View File

@ -1,20 +1,60 @@
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from snek.forms import RegisterForm from snek.forms import RegisterForm
from aiohttp import web from aiohttp import web
import aiohttp
import pathlib
from snek import http
from snek.middleware import cors_allow_middleware,cors_middleware
class Application(BaseApplication): class Application(BaseApplication):
def __init__(self, *args, **kwargs): 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("/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_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): async def handle_register(self, request):
if request.method == "GET": 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": elif request.method == "POST":
return self.render("register.html") return self.render("register.html")
app = Application()
if __name__ == '__main__': if __name__ == '__main__':
app = Application()
web.run_app(app,port=8081,host="0.0.0.0") 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>