Progress.
This commit is contained in:
parent
46a27405ae
commit
a7446d1314
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
40
Dockerfile
Normal 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"]
|
6
Makefile
6
Makefile
@ -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
0
cache/crc321300331366.cache
vendored
Normal file
0
cache/crc322507170282.cache
vendored
Normal file
0
cache/crc322507170282.cache
vendored
Normal file
12
compose.yml
Normal file
12
compose.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
snek:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
volumes:
|
||||||
|
- ./:/code
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
- action: sync
|
||||||
|
path: .
|
||||||
|
target: /code
|
@ -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
|
||||||
|
@ -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
3
src/snek/gunicorn.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from snek.app import app
|
||||||
|
|
||||||
|
application = app
|
83
src/snek/http.py
Normal file
83
src/snek/http.py
Normal 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
32
src/snek/middleware.py
Normal 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
77
src/snek/static/app.js
Normal 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"))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
9
src/snek/static/html_frame.css
Normal file
9
src/snek/static/html_frame.css
Normal 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;
|
||||||
|
|
||||||
|
}
|
39
src/snek/static/html_frame.js
Normal file
39
src/snek/static/html_frame.js
Normal 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);
|
172
src/snek/static/prachtig-gitter_like.html
Normal file
172
src/snek/static/prachtig-gitter_like.html
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
95
src/snek/static/register.css
Normal file
95
src/snek/static/register.css
Normal 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
203
src/snek/static/styles.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
20
src/snek/templates/login.html
Normal file
20
src/snek/templates/login.html
Normal 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>
|
51
src/snek/templates/prachtig_gitter_like.html
Normal file
51
src/snek/templates/prachtig_gitter_like.html
Normal 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>
|
||||||
|
|
22
src/snek/templates/register.html
Normal file
22
src/snek/templates/register.html
Normal 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>
|
63
src/snek/templates/test.html
Normal file
63
src/snek/templates/test.html
Normal 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>
|
||||||
|
|
122
src/snek/templates/test2.html
Normal file
122
src/snek/templates/test2.html
Normal 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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user