Progress.
This commit is contained in:
parent
46a27405ae
commit
a7446d1314
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,7 @@
|
||||
.vscode
|
||||
.history
|
||||
*.db*
|
||||
|
||||
*.png
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__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
|
||||
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
|
||||
|
||||
|
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
|
||||
install_requires =
|
||||
app @ git+https://retoor.molodetz.nl/retoor/app
|
||||
beautifulsoup4
|
||||
gunicorn
|
||||
imgkit
|
||||
wkhtmltopdf
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
@ -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")
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = Application()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
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