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