Compare commits
52 commits
Author | SHA1 | Date | |
---|---|---|---|
8fec9e6ef7 | |||
![]() |
45c1d0f9cc | ||
![]() |
c2334642fb | ||
![]() |
6e98d1979c | ||
1171e0de40 | |||
![]() |
360164be7a | ||
![]() |
e5e61fb9c4 | ||
3b0a40ace8 | |||
![]() |
060560d8a2 | ||
![]() |
0fa4e48ff5 | ||
![]() |
62699c90eb | ||
![]() |
6b3783e69f | ||
![]() |
15d914fae3 | ||
![]() |
bf182251ec | ||
![]() |
0c1a280a19 | ||
![]() |
d98342bd02 | ||
ebf06fa163 | |||
![]() |
7236d859eb | ||
![]() |
f8ecf20b75 | ||
![]() |
065fa5adbf | ||
![]() |
1b3de458fa | ||
![]() |
c9cda14f38 | ||
![]() |
9f95e704b9 | ||
b9e87f09d5 | |||
![]() |
5b5728f7e2 | ||
457af44a79 | |||
9935c0562b | |||
7b51b3414d | |||
a6e363220a | |||
![]() |
299a2f5914 | ||
b6a0d22674 | |||
b41f74bf7b | |||
c25188611e | |||
2ec7600fa4 | |||
![]() |
2aae5dd441 | ||
![]() |
f91b83d3ca | ||
![]() |
6cdeb1a3da | ||
0bc3cac6f5 | |||
277b2aec57 | |||
111a0f6e45 | |||
![]() |
703a210f1e | ||
![]() |
ec64e81829 | ||
![]() |
8f5a82fb41 | ||
![]() |
ea0538f90c | ||
![]() |
2b7342b902 | ||
![]() |
6d01523d8d | ||
![]() |
621db6ad2c | ||
![]() |
f1384b72f2 | ||
![]() |
f3658a76ff | ||
![]() |
1bf4eb801f | ||
![]() |
99df802a40 | ||
![]() |
eba9b410c6 |
21 changed files with 794 additions and 55 deletions
|
@ -1,6 +1,6 @@
|
||||||
/.dockerignore
|
.*
|
||||||
/.gitignore
|
|
||||||
/inventory.sqlite3
|
/inventory.sqlite3
|
||||||
/README.md
|
/README.md
|
||||||
*.pyc
|
*.pyc
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
/contrib/
|
||||||
|
|
22
.forgejo/workflows/deployment-pull-request.yaml
Normal file
22
.forgejo/workflows/deployment-pull-request.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: deployment-on-pull-request
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build inventory
|
||||||
|
uses: https://codeberg.org/umglurf/kaniko-action@main
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
push: 'false'
|
||||||
|
context: /workspace/snoopy/in-c4/
|
24
.forgejo/workflows/deployment.yaml
Normal file
24
.forgejo/workflows/deployment.yaml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
name: deployment
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build inventory
|
||||||
|
uses: https://codeberg.org/umglurf/kaniko-action@main
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
cache_repo: git.koeln.ccc.de/${{ github.repository }}/cache
|
||||||
|
push: 'true'
|
||||||
|
context: /workspace/snoopy/in-c4/
|
||||||
|
credentials: |
|
||||||
|
git.koeln.ccc.de=snoopy:${{ secrets.REGISTRY_WRITE }}
|
||||||
|
destinations: |
|
||||||
|
git.koeln.ccc.de/${{ github.repository }}/in-c4:latest
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/inventory.sqlite3
|
/data/*
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.10-alpine
|
FROM python:3.12-alpine3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
47
contrib/bulk_create.py
Executable file
47
contrib/bulk_create.py
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE = "https://in.ccc.ac"
|
||||||
|
COUNT = 0
|
||||||
|
START_ID = 1
|
||||||
|
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
# session.auth = (":)", ":)")
|
||||||
|
|
||||||
|
res = session.get(f"{BASE}/api/items")
|
||||||
|
res.raise_for_status()
|
||||||
|
ids = res.json().keys()
|
||||||
|
kiste_ids = {int(i.lstrip("K")) for i in ids if i.startswith("K") and i != "K"}
|
||||||
|
print("existing ids:", repr(kiste_ids), file=sys.stderr)
|
||||||
|
|
||||||
|
current_id = START_ID
|
||||||
|
new_ids = []
|
||||||
|
while len(new_ids) < COUNT:
|
||||||
|
if current_id not in kiste_ids:
|
||||||
|
new_ids.append(current_id)
|
||||||
|
current_id += 1
|
||||||
|
|
||||||
|
print("created:", file=sys.stderr)
|
||||||
|
if len(new_ids) == 0:
|
||||||
|
print("(none)", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
for i in new_ids:
|
||||||
|
j = {
|
||||||
|
"id": f"K{i}",
|
||||||
|
"is_in": None,
|
||||||
|
"coords_bl": None,
|
||||||
|
"coords_tr": None,
|
||||||
|
"type": "Kiste",
|
||||||
|
"name": None,
|
||||||
|
"content": None,
|
||||||
|
"note": "Bitte Name, Ort, Inhalt eintragen",
|
||||||
|
"hidden": False,
|
||||||
|
}
|
||||||
|
res = session.put(f"{BASE}/api/items/{j['id']}", json=j)
|
||||||
|
res.raise_for_status()
|
||||||
|
print(f"{j['id']},{BASE}/form.html?id={j['id']}")
|
24
crud.py
24
crud.py
|
@ -1,18 +1,32 @@
|
||||||
from sqlalchemy.orm import Session
|
from enum import Enum
|
||||||
from sqlalchemy import DECIMAL, cast
|
|
||||||
|
from sqlalchemy import DECIMAL, cast
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
import models
|
||||||
|
import schemas
|
||||||
|
|
||||||
import models, schemas
|
|
||||||
|
|
||||||
def get_items(db: Session) -> list[models.Item]:
|
def get_items(db: Session) -> list[models.Item]:
|
||||||
return db.query(models.Item).all()
|
return db.query(models.Item).all()
|
||||||
|
|
||||||
|
|
||||||
def put_item(db: Session, id: str, item: schemas.Item):
|
def put_item(db: Session, id: str, item: schemas.Item):
|
||||||
updated = bool(db.query(models.Item).filter(models.Item.id == id).update(item.dict()))
|
updated = bool(
|
||||||
|
db.query(models.Item).filter(models.Item.id == id).update(item.dict())
|
||||||
|
)
|
||||||
if not updated:
|
if not updated:
|
||||||
db.add(models.Item(**item.dict()))
|
db.add(models.Item(**item.dict()))
|
||||||
db.commit()
|
db.commit()
|
||||||
return updated
|
if updated:
|
||||||
|
return PutItemResult.UPDATED
|
||||||
|
else:
|
||||||
|
return PutItemResult.ADDED
|
||||||
|
|
||||||
|
|
||||||
|
class PutItemResult(Enum):
|
||||||
|
ADDED = 1
|
||||||
|
UPDATED = 2
|
||||||
|
|
||||||
|
|
||||||
def delete_item(db: Session, id: str):
|
def delete_item(db: Session, id: str):
|
||||||
|
|
|
@ -2,12 +2,10 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./inventory.sqlite3"
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/inventory.sqlite3"
|
||||||
|
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
SQLALCHEMY_DATABASE_URL,
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, echo=True
|
||||||
connect_args = {"check_same_thread": False},
|
|
||||||
echo=True
|
|
||||||
)
|
)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
35
main.py
35
main.py
|
@ -1,9 +1,12 @@
|
||||||
|
import re
|
||||||
|
from calendar import timegm
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Response
|
from fastapi import Depends, FastAPI, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
import re
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
import crud
|
import crud
|
||||||
from database import SessionLocal, engine
|
from database import SessionLocal, engine
|
||||||
|
@ -14,6 +17,7 @@ Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
@ -21,6 +25,7 @@ def get_db():
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(Engine, "connect")
|
@event.listens_for(Engine, "connect")
|
||||||
def _set_sqlite_pragma(conn, _):
|
def _set_sqlite_pragma(conn, _):
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
@ -31,21 +36,35 @@ def _set_sqlite_pragma(conn, _):
|
||||||
@app.get("/api/items", response_model=dict[str, Item])
|
@app.get("/api/items", response_model=dict[str, Item])
|
||||||
async def list_items(db: Session = Depends(get_db)):
|
async def list_items(db: Session = Depends(get_db)):
|
||||||
# natural sort by id
|
# natural sort by id
|
||||||
natsort = lambda item: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', item.id)]
|
natsort = lambda item: [
|
||||||
|
int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", item.id)
|
||||||
|
]
|
||||||
items = crud.get_items(db)
|
items = crud.get_items(db)
|
||||||
items = sorted(items, key=natsort)
|
items = sorted(items, key=natsort)
|
||||||
return {i.id:i for i in items}
|
return {i.id: i for i in items}
|
||||||
|
|
||||||
|
|
||||||
@app.put("/api/items/{id}")
|
@app.put(
|
||||||
|
"/api/items/{id}",
|
||||||
|
status_code=201,
|
||||||
|
responses={201: {"description": "created"}, 204: {"description": "updated"}},
|
||||||
|
)
|
||||||
async def put_item(id: str, item: Item, db: Session = Depends(get_db)):
|
async def put_item(id: str, item: Item, db: Session = Depends(get_db)):
|
||||||
if crud.put_item(db, id, item):
|
if item.last_updated is None:
|
||||||
return Response(b'', status_code=204)
|
item.last_updated = month_timestamp()
|
||||||
return Response(b'', status_code=201)
|
if crud.put_item(db, id, item) == crud.PutItemResult.UPDATED:
|
||||||
|
return Response(b"", status_code=204)
|
||||||
|
return Response(b"", status_code=201)
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/items/{id}", status_code=204)
|
@app.delete("/api/items/{id}", status_code=204)
|
||||||
async def delete_item(id: str, db: Session = Depends(get_db)):
|
async def delete_item(id: str, db: Session = Depends(get_db)):
|
||||||
crud.delete_item(db, id)
|
crud.delete_item(db, id)
|
||||||
|
|
||||||
|
|
||||||
|
def month_timestamp() -> int:
|
||||||
|
"""Provides the timestamp of the current month's beginning (for improved privacy)"""
|
||||||
|
return timegm(date.today().replace(day=1).timetuple())
|
||||||
|
|
||||||
|
|
||||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from sqlalchemy import Column, ForeignKey, String, Text, Boolean
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
|
||||||
|
|
||||||
from database import Base
|
from database import Base
|
||||||
|
|
||||||
|
|
||||||
class Item(Base):
|
class Item(Base):
|
||||||
__tablename__ = "items"
|
__tablename__ = "items"
|
||||||
|
|
||||||
|
@ -14,3 +15,4 @@ class Item(Base):
|
||||||
content = Column(Text)
|
content = Column(Text)
|
||||||
note = Column(Text)
|
note = Column(Text)
|
||||||
hidden = Column(Boolean, nullable=False)
|
hidden = Column(Boolean, nullable=False)
|
||||||
|
last_updated = Column(Integer)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
fastapi>=0.79.0
|
fastapi>=0.109.0
|
||||||
sqlalchemy>=1.4.39
|
sqlalchemy>=2.0.25
|
||||||
uvicorn>=0.17.6
|
uvicorn>=0.27.0.post1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Item(BaseModel):
|
class Item(BaseModel):
|
||||||
|
@ -11,6 +11,7 @@ class Item(BaseModel):
|
||||||
content: str | None
|
content: str | None
|
||||||
note: str | None
|
note: str | None
|
||||||
hidden: bool
|
hidden: bool
|
||||||
|
last_updated: int | None = Field(None)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 60 KiB |
122
static/c4.css
Normal file
122
static/c4.css
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
body {
|
||||||
|
color: #f0f0f0;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h1 a {
|
||||||
|
color: #dfaa37; /* C4 Yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
#search.failed {
|
||||||
|
color: #200;
|
||||||
|
background-color: #ec6d5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
border: 1px solid #202020;
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
max-width: 1440px;
|
||||||
|
max-height: 50vh;
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
|
box-shadow: 0px 6px 12px #000c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mapnote {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #333;
|
||||||
|
border-color: #202020;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
box-shadow: 0px 6px 12px #000c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mapnote:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#itemcount {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
background-color: #202020;
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result:focus {
|
||||||
|
background-color: #202020;
|
||||||
|
border-color: #dfaa37;
|
||||||
|
outline-color: #dfaa37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result a {
|
||||||
|
color: #dfaa37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
background-color: #333;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
#knowntypes button {
|
||||||
|
color: #220;
|
||||||
|
background-color: #dfaa37;
|
||||||
|
border: 1px solid #220;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0.2em;
|
||||||
|
font-weight: bolder;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#knowntypes button:hover, #knowntypes button:focus {
|
||||||
|
background-color: #f5c251;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
box-shadow: 0 3px 6px -1px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
color: #220;
|
||||||
|
background-color: #dfaa37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover,.btn.primary:focus {
|
||||||
|
background-color: #f5c251;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.grn {
|
||||||
|
color: #020;
|
||||||
|
background-color: #9f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.grn:hover ,.btn.grn:focus {
|
||||||
|
background-color: #3f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.red {
|
||||||
|
color: #200;
|
||||||
|
background-color: #ec6d5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.red:hover, .btn.red:focus {
|
||||||
|
background-color: #ff7667;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinker {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
40% { opacity: 1; }
|
||||||
|
90% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mapgrid {
|
||||||
|
outline: 2px solid #333;
|
||||||
|
background-color: #dfaa37;
|
||||||
|
border-radius: min(4px, 25%);
|
||||||
|
animation: blinker 1s infinite;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>in?</title>
|
<title>in?</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="c4.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="form.js"></script>
|
<script src="form.js"></script>
|
||||||
<script src="shared.js"></script>
|
<script src="shared.js"></script>
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
<b>Coordinates</b> (click on map)
|
<b>Coordinates</b> (click on map)
|
||||||
|
|
||||||
<div id="map" title="Map for reference only. Not to scale!">
|
<div id="map" title="Map for reference only. Not to scale!">
|
||||||
<img src="80x80.svg" onclick="mapClick(event)">
|
<img src="map.svg" onclick="mapClick(event)">
|
||||||
<div id="mapgrid"></div>
|
<div id="mapgrid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -43,9 +44,12 @@
|
||||||
|
|
||||||
<label for="hidden">Hide by default</label>
|
<label for="hidden">Hide by default</label>
|
||||||
<input type="checkbox" id="hidden">
|
<input type="checkbox" id="hidden">
|
||||||
|
|
||||||
|
<label for="last_updated">Last updated</label>
|
||||||
|
<input id="last_updated" type="text" disabled>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<button id="delete" class="btn red" autocomplete="off">Delete</button>
|
<button id="delete" class="btn red" autocomplete="off">Delete</button>
|
||||||
<button id="save" class="btn green" autocomplete="off">Save</button>
|
<button id="save" class="btn primary" autocomplete="off">Save</button>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -39,6 +39,7 @@ function fillForm() {
|
||||||
document.getElementById('content').value = item.content;
|
document.getElementById('content').value = item.content;
|
||||||
document.getElementById('note').value = item.note;
|
document.getElementById('note').value = item.note;
|
||||||
document.getElementById('hidden').checked = item.hidden;
|
document.getElementById('hidden').checked = item.hidden;
|
||||||
|
document.getElementById('last_updated').value = formatTimestamp(item.last_updated);
|
||||||
|
|
||||||
formCoordsToMap();
|
formCoordsToMap();
|
||||||
}
|
}
|
||||||
|
@ -64,8 +65,22 @@ function knownTypes() {
|
||||||
map[i.type] = true;
|
map[i.type] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let types = Object.keys(map).sort().join(', ')
|
|
||||||
document.getElementById('knowntypes').textContent = `Known types: ${types}.`;
|
// Get location in document.
|
||||||
|
const known_types = document.getElementById('knowntypes');
|
||||||
|
known_types.textContent = `Known types: `;
|
||||||
|
|
||||||
|
// Create a button for every known type.
|
||||||
|
for (let label of Object.keys(map).sort()) {
|
||||||
|
btn = document.createElement('button');
|
||||||
|
btn.setAttribute('type', 'button');
|
||||||
|
btn.textContent = label;
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
document.getElementById('type').value = label;
|
||||||
|
});
|
||||||
|
|
||||||
|
known_types.appendChild(btn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -135,14 +150,14 @@ function del() {
|
||||||
|
|
||||||
let clicks = {x: [], y: []};
|
let clicks = {x: [], y: []};
|
||||||
function mapClick(e) {
|
function mapClick(e) {
|
||||||
let x = Math.floor(31 / e.target.width * e.layerX);
|
let x = Math.floor(36 / e.target.width * e.layerX);
|
||||||
let y = Math.floor(10 / e.target.height * e.layerY);
|
let y = Math.floor(18 / e.target.height * e.layerY);
|
||||||
|
|
||||||
let humanPos = (x, y) => {
|
let humanPos = (x, y) => {
|
||||||
return `${String.fromCharCode(65 + 8 - y)}${x}`;
|
return `${String.fromCharCode(65 + 16 - y)}${x}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (x > 0 && x < 31 && y > 0 && y < 9) {
|
if (x > 0 && x < 36 && y > 0 && y < 18) {
|
||||||
if (clicks.x.length > 1) {
|
if (clicks.x.length > 1) {
|
||||||
clicks.x = [x];
|
clicks.x = [x];
|
||||||
clicks.y = [y];
|
clicks.y = [y];
|
||||||
|
@ -172,3 +187,9 @@ function formCoordsToMap() {
|
||||||
coordsToMap(coords_bl, coords_tr);
|
coordsToMap(coords_bl, coords_tr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(ts) {
|
||||||
|
const date = new Date(ts * 1000);
|
||||||
|
// using Swedish format as a hack to get an iso formatted date
|
||||||
|
return date.toLocaleDateString("sv", {timeZone: "UTC"}).replace(/\-\d+$/,'')
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>in?</title>
|
<title>in?</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="c4.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||
<script src="shared.js"></script>
|
<script src="shared.js"></script>
|
||||||
|
@ -11,12 +12,13 @@
|
||||||
<div id="grid">
|
<div id="grid">
|
||||||
<div id="searchcontainer">
|
<div id="searchcontainer">
|
||||||
<input type="text" id="search" value="" autocomplete="off" placeholder="RegEx search" tabindex="1" autofocus>
|
<input type="text" id="search" value="" autocomplete="off" placeholder="RegEx search" tabindex="1" autofocus>
|
||||||
|
<p id="itemcount"></p>
|
||||||
<label for="showhidden">Show hidden items</label>
|
<label for="showhidden">Show hidden items</label>
|
||||||
<input type="checkbox" id="showhidden">
|
<input type="checkbox" id="showhidden">
|
||||||
</div>
|
</div>
|
||||||
<div id="mapcontainer">
|
<div id="mapcontainer">
|
||||||
<div id="map">
|
<div id="map">
|
||||||
<img src="80x80.svg" title="Map for reference only. Not to scale!">
|
<img src="map.svg" title="Map for reference only. Not to scale!">
|
||||||
<div id="mapgrid"></div>
|
<div id="mapgrid"></div>
|
||||||
</div>
|
</div>
|
||||||
<p id="mapnote"></p>
|
<p id="mapnote"></p>
|
||||||
|
@ -37,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<a href="form.html" class="btn green">+</a>
|
<a href="form.html" class="btn primary">+</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -17,6 +17,7 @@ function renderItems() {
|
||||||
const container = document.getElementById('results');
|
const container = document.getElementById('results');
|
||||||
const template = document.getElementById('item');
|
const template = document.getElementById('item');
|
||||||
const loading = document.getElementById('loading');
|
const loading = document.getElementById('loading');
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
for (const [id, item] of Object.entries(items)) {
|
for (const [id, item] of Object.entries(items)) {
|
||||||
const clone = template.content.cloneNode(true);
|
const clone = template.content.cloneNode(true);
|
||||||
|
@ -31,11 +32,14 @@ function renderItems() {
|
||||||
clone.querySelector(".result").id = `item-${id}`;
|
clone.querySelector(".result").id = `item-${id}`;
|
||||||
clone.querySelector("a").href = `form.html?id=${encodeURIComponent(id)}`;
|
clone.querySelector("a").href = `form.html?id=${encodeURIComponent(id)}`;
|
||||||
if (item.hidden) {
|
if (item.hidden) {
|
||||||
clone.querySelector(".result").classList.add('hidden')
|
clone.querySelector(".result").classList.add('hidden');
|
||||||
};
|
} else {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
container.appendChild(clone);
|
container.appendChild(clone);
|
||||||
};
|
}
|
||||||
loading.remove();
|
loading.remove();
|
||||||
|
updateCounter(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocString(items, item) {
|
function getLocString(items, item) {
|
||||||
|
@ -67,6 +71,7 @@ function search(e) {
|
||||||
const searchAttrs = ['id', 'name', 'type', 'note', 'content'];
|
const searchAttrs = ['id', 'name', 'type', 'note', 'content'];
|
||||||
const query = e.target.value;
|
const query = e.target.value;
|
||||||
const regex = new RegExp(query, 'i')
|
const regex = new RegExp(query, 'i')
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
for (const elem of document.getElementsByClassName('result')) {
|
for (const elem of document.getElementsByClassName('result')) {
|
||||||
const item = items[elem.id.slice(5)];
|
const item = items[elem.id.slice(5)];
|
||||||
|
@ -85,10 +90,36 @@ function search(e) {
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
elem.classList.remove('filtered');
|
elem.classList.remove('filtered');
|
||||||
|
count++;
|
||||||
} else {
|
} else {
|
||||||
elem.classList.add('filtered');
|
elem.classList.add('filtered');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Indicate failed search.
|
||||||
|
if (count) {
|
||||||
|
e.target.classList.remove('failed');
|
||||||
|
} else {
|
||||||
|
e.target.classList.add('failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounter(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCounter(count) {
|
||||||
|
const itemcount = document.getElementById('itemcount');
|
||||||
|
|
||||||
|
switch(count) {
|
||||||
|
case 0:
|
||||||
|
itemcount.textContent = 'No items found.';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
itemcount.textContent = '1 item found.';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
itemcount.textContent = `${count} items found.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showhidden(e){
|
function showhidden(e){
|
||||||
|
@ -118,4 +149,4 @@ function showItem(e) {
|
||||||
|
|
||||||
function hideItem(e) {
|
function hideItem(e) {
|
||||||
clearMap();
|
clearMap();
|
||||||
}
|
}
|
||||||
|
|
433
static/map.svg
Normal file
433
static/map.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 96 KiB |
|
@ -1,12 +1,12 @@
|
||||||
function renderMap(xx, yy) {
|
function renderMap(xx, yy) {
|
||||||
if (xx[0] > 0 && xx[0] < 31 && yy[0] > 0 && yy[0] < 9 &&
|
if (xx[0] > 0 && xx[0] < 36 && yy[0] > 0 && yy[0] < 18 &&
|
||||||
xx[1] > 0 && xx[1] < 31 && yy[1] > 0 && yy[1] < 9 &&
|
xx[1] > 0 && xx[1] < 36 && yy[1] > 0 && yy[1] < 18 &&
|
||||||
xx[1] >= xx[0] && yy[1] >= yy[0]) {
|
xx[1] >= xx[0] && yy[1] >= yy[0]) {
|
||||||
let grid = document.getElementById('mapgrid');
|
let grid = document.getElementById('mapgrid');
|
||||||
grid.style.top = `${yy[0]*10}%`;
|
grid.style.top = `${yy[0]*100/18}%`;
|
||||||
grid.style.left = `${100/31*xx[0]}%`;
|
grid.style.left = `${100/36*xx[0]}%`;
|
||||||
grid.style.height = `${(yy[1] - yy[0] + 1) * 10}%`;
|
grid.style.height = `${(yy[1] - yy[0] + 1) * 100/18}%`;
|
||||||
grid.style.width = `${100/31*(xx[1] - xx[0] + 1)}%`;
|
grid.style.width = `${100/36*(xx[1] - xx[0] + 1)}%`;
|
||||||
} else {
|
} else {
|
||||||
alert(`invalid coordinates x=${xx} y=${yy}`);
|
alert(`invalid coordinates x=${xx} y=${yy}`);
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ function coordsToMap(coords_bl, coords_tr) {
|
||||||
let bl_x = parseInt(coords_bl.slice(1));
|
let bl_x = parseInt(coords_bl.slice(1));
|
||||||
let tr_x = parseInt(coords_tr.slice(1));
|
let tr_x = parseInt(coords_tr.slice(1));
|
||||||
|
|
||||||
bl_y = 8 - (bl_y.charCodeAt(0) - 65);
|
bl_y = 16 - (bl_y.charCodeAt(0) - 65);
|
||||||
tr_y = 8 - (tr_y.charCodeAt(0) - 65);
|
tr_y = 16 - (tr_y.charCodeAt(0) - 65);
|
||||||
|
|
||||||
renderMap([bl_x, tr_x], [tr_y, bl_y]);
|
renderMap([bl_x, tr_x], [tr_y, bl_y]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,10 +171,10 @@ textarea {
|
||||||
#map {
|
#map {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
background: #fff;
|
background: #4d4d4d;
|
||||||
max-width: 800px;
|
max-width: 1440px;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
aspect-ratio: 31 / 10;
|
aspect-ratio: 2 / 1;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,4 +238,4 @@ form #map {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue