feat: add Dockerfile, compose.yml, gunicorn config, http utilities, and static assets
Add Dockerfile with wkhtmltopdf multi-stage build and gunicorn entrypoint for production deployment. Introduce compose.yml with volume sync for development. Switch Makefile from snek.serve to gunicorn with aiohttp worker. Extend Application class with CORS middleware, static file serving, login/test/http-get/http-photo routes, and template rendering. Create gunicorn.py entrypoint, http.py module with async URL fetching, screenshot generation via imgkit, link repair, and CRC32-based caching. Add middleware.py with CORS handlers. Include static assets: app.js with Message/Room/App classes, html_frame.css/js custom element, and prachtig-gitter_like.html UI template. Update setup.cfg dependencies with beautifulsoup4, gunicorn, imgkit, wkhtmltopdf. Add *.png to .gitignore.
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
.vscode
|
||||
.history
|
||||
*.db*
|
||||
|
||||
*.png
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
+40
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
Vendored
Vendored
+12
@@ -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
|
||||
install_requires =
|
||||
app @ git+https://retoor.molodetz.nl/retoor/app
|
||||
beautifulsoup4
|
||||
gunicorn
|
||||
imgkit
|
||||
wkhtmltopdf
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
+43
-3
@@ -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")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from snek.app import app
|
||||
|
||||
application = app
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user