From a7446d131413da9f013a56d3541192d8ab1e22b0 Mon Sep 17 00:00:00 2001 From: retoor <retoor@molodetz.nl> Date: Sat, 18 Jan 2025 13:21:38 +0100 Subject: [PATCH] Progress. --- .gitignore | 2 +- Dockerfile | 40 ++++ Makefile | 6 +- cache/crc321300331366.cache | 0 cache/crc322507170282.cache | 0 compose.yml | 12 ++ setup.cfg | 4 + src/snek/app.py | 46 ++++- src/snek/gunicorn.py | 3 + src/snek/http.py | 83 ++++++++ src/snek/middleware.py | 32 +++ src/snek/static/app.js | 77 +++++++ src/snek/static/html_frame.css | 9 + src/snek/static/html_frame.js | 39 ++++ src/snek/static/prachtig-gitter_like.html | 172 ++++++++++++++++ src/snek/static/register.css | 95 +++++++++ src/snek/static/styles.css | 203 +++++++++++++++++++ src/snek/templates/login.html | 20 ++ src/snek/templates/prachtig_gitter_like.html | 51 +++++ src/snek/templates/register.html | 22 ++ src/snek/templates/test.html | 63 ++++++ src/snek/templates/test2.html | 122 +++++++++++ 22 files changed, 1095 insertions(+), 6 deletions(-) create mode 100644 Dockerfile create mode 100644 cache/crc321300331366.cache create mode 100644 cache/crc322507170282.cache create mode 100644 compose.yml create mode 100644 src/snek/gunicorn.py create mode 100644 src/snek/http.py create mode 100644 src/snek/middleware.py create mode 100644 src/snek/static/app.js create mode 100644 src/snek/static/html_frame.css create mode 100644 src/snek/static/html_frame.js create mode 100644 src/snek/static/prachtig-gitter_like.html create mode 100644 src/snek/static/register.css create mode 100644 src/snek/static/styles.css create mode 100644 src/snek/templates/login.html create mode 100644 src/snek/templates/prachtig_gitter_like.html create mode 100644 src/snek/templates/register.html create mode 100644 src/snek/templates/test.html create mode 100644 src/snek/templates/test2.html diff --git a/.gitignore b/.gitignore index 8cb2598..3747073 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .vscode .history *.db* - +*.png # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76436ba --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile index d41b81a..65c58fa 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cache/crc321300331366.cache b/cache/crc321300331366.cache new file mode 100644 index 0000000..e69de29 diff --git a/cache/crc322507170282.cache b/cache/crc322507170282.cache new file mode 100644 index 0000000..e69de29 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..9186108 --- /dev/null +++ b/compose.yml @@ -0,0 +1,12 @@ +services: + snek: + build: . + ports: + - "8081:8081" + volumes: + - ./:/code + develop: + watch: + - action: sync + path: . + target: /code \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index bb6480d..ca8353d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/src/snek/app.py b/src/snek/app.py index feb2fc0..97fbb96 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -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") +app = Application() + if __name__ == '__main__': - app = Application() + web.run_app(app,port=8081,host="0.0.0.0") diff --git a/src/snek/gunicorn.py b/src/snek/gunicorn.py new file mode 100644 index 0000000..4055bbd --- /dev/null +++ b/src/snek/gunicorn.py @@ -0,0 +1,3 @@ +from snek.app import app + +application = app diff --git a/src/snek/http.py b/src/snek/http.py new file mode 100644 index 0000000..0b16bee --- /dev/null +++ b/src/snek/http.py @@ -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 \ No newline at end of file diff --git a/src/snek/middleware.py b/src/snek/middleware.py new file mode 100644 index 0000000..6b801ab --- /dev/null +++ b/src/snek/middleware.py @@ -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 \ No newline at end of file diff --git a/src/snek/static/app.js b/src/snek/static/app.js new file mode 100644 index 0000000..701beda --- /dev/null +++ b/src/snek/static/app.js @@ -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")) + + + } + + + +} \ No newline at end of file diff --git a/src/snek/static/html_frame.css b/src/snek/static/html_frame.css new file mode 100644 index 0000000..6b64c76 --- /dev/null +++ b/src/snek/static/html_frame.css @@ -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; + +} \ No newline at end of file diff --git a/src/snek/static/html_frame.js b/src/snek/static/html_frame.js new file mode 100644 index 0000000..19a0c34 --- /dev/null +++ b/src/snek/static/html_frame.js @@ -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); \ No newline at end of file diff --git a/src/snek/static/prachtig-gitter_like.html b/src/snek/static/prachtig-gitter_like.html new file mode 100644 index 0000000..bb30554 --- /dev/null +++ b/src/snek/static/prachtig-gitter_like.html @@ -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; + } +} + diff --git a/src/snek/static/register.css b/src/snek/static/register.css new file mode 100644 index 0000000..fc4ca0f --- /dev/null +++ b/src/snek/static/register.css @@ -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%; + } + } + \ No newline at end of file diff --git a/src/snek/static/styles.css b/src/snek/static/styles.css new file mode 100644 index 0000000..83c1fb1 --- /dev/null +++ b/src/snek/static/styles.css @@ -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; + } +} + diff --git a/src/snek/templates/login.html b/src/snek/templates/login.html new file mode 100644 index 0000000..d37f3fa --- /dev/null +++ b/src/snek/templates/login.html @@ -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> diff --git a/src/snek/templates/prachtig_gitter_like.html b/src/snek/templates/prachtig_gitter_like.html new file mode 100644 index 0000000..cd2d863 --- /dev/null +++ b/src/snek/templates/prachtig_gitter_like.html @@ -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> + diff --git a/src/snek/templates/register.html b/src/snek/templates/register.html new file mode 100644 index 0000000..da41629 --- /dev/null +++ b/src/snek/templates/register.html @@ -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> diff --git a/src/snek/templates/test.html b/src/snek/templates/test.html new file mode 100644 index 0000000..c23103a --- /dev/null +++ b/src/snek/templates/test.html @@ -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> + diff --git a/src/snek/templates/test2.html b/src/snek/templates/test2.html new file mode 100644 index 0000000..9aad83a --- /dev/null +++ b/src/snek/templates/test2.html @@ -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> +