Init
This commit is contained in:
commit
2421fcf900
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
AUTH0_DOMAIN = your.domain.auth0.com
|
||||||
|
AUTH0_API_AUDIENCE = your.api.audience
|
||||||
|
AUTH0_ISSUER = https://your.domain.auth0.com/
|
||||||
|
AUTH0_ALGORITHMS = RS256
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
|
.idea
|
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Auth0 + Python + FastAPI API Seed
|
||||||
|
|
||||||
|
This is the seed project you need to use if you're going to create an API using FastAPI in Python and Auth0. If you just want to create a Regular Python WebApp, please check [this project](https://github.com/auth0-samples/auth0-python-web-app/tree/master/01-Login)
|
||||||
|
|
||||||
|
## Running the example
|
||||||
|
|
||||||
|
In order to run the example you need to have `python3` (any version higher than `3.6`) and `pip3` installed.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The configuration you'll need is mostly information from Auth0, you'll need both the tentant domain and the API information.
|
||||||
|
|
||||||
|
This app reads its configuration information from a `.env` file by default.
|
||||||
|
|
||||||
|
To create a `.env` file you can copy the `.env.example` file and fill the values accordingly:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can use environment variables to define your application's settings (remember to update the values accordingly):
|
||||||
|
|
||||||
|
```console
|
||||||
|
export AUTH0_DOMAIN='your.domain.auth0.com'
|
||||||
|
export AUTH0_API_AUDIENCE='your.api.audience'
|
||||||
|
export AUTH0_ISSUER='https://your.domain.auth0.com'
|
||||||
|
export AUTH0_ALGORITHMS='RS256'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spin up the server
|
||||||
|
|
||||||
|
Once you've set your environment information below you'll find the commands you'll need.
|
||||||
|
|
||||||
|
1. Create and activate a python environment:
|
||||||
|
|
||||||
|
```console
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the needed dependencies with:
|
||||||
|
|
||||||
|
```console
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. Start the server with the following:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uvicorn application.main:app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Try calling [http://localhost:8000/api/public](http://localhost:8000/api/public)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X 'GET' \
|
||||||
|
'http://localhost:8000/api/public' \
|
||||||
|
-H 'accept: application/json'
|
||||||
|
```
|
||||||
|
|
||||||
|
## API documentation
|
||||||
|
|
||||||
|
Access [http://localhost:8000/docs](http://localhost:8000/docs). From there you'll see all endpoints and can test your API
|
||||||
|
|
||||||
|
### Testing the API
|
||||||
|
|
||||||
|
#### Private endpoint
|
||||||
|
|
||||||
|
You can then try to do a GET to [http://localhost:8000/api/private](http://localhost:8000/api/private) which will throw an error if you don't send an access token signed with RS256 with the appropriate issuer and audience in the Authorization header.
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -X 'GET' \
|
||||||
|
'http://localhost:8000/api/private' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'Authorization: Bearer <FILL YOUR TOKEN HERE>'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Private-Scoped endpoint
|
||||||
|
|
||||||
|
You can also try to do a GET to [http://localhost:8000/api/private-scoped](http://localhost:8000/api/private-scoped) which will throw an error if you don't send an access token with the scope `read:messages` signed with RS256 with the appropriate issuer and audience in the Authorization header.
|
||||||
|
|
||||||
|
```console
|
||||||
|
curl -X 'GET' \
|
||||||
|
'http://localhost:8000/api/private-scoped' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H 'Authorization: Bearer <FILL YOUR TOKEN WITH SCOPES HERE>'
|
||||||
|
```
|
BIN
application/.DS_Store
vendored
Normal file
BIN
application/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
application/IBMPlexMono-Bold.ttf
Normal file
BIN
application/IBMPlexMono-Bold.ttf
Normal file
Binary file not shown.
BIN
application/IBMPlexMono-Medium.ttf
Normal file
BIN
application/IBMPlexMono-Medium.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono.zip
Normal file
BIN
application/IBM_Plex_Mono.zip
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/.DS_Store
vendored
Normal file
BIN
application/IBM_Plex_Mono/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-Italic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-Italic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-Light.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-Light.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-Regular.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-Regular.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-Thin.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-Thin.ttf
Normal file
Binary file not shown.
BIN
application/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf
Normal file
BIN
application/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf
Normal file
Binary file not shown.
93
application/IBM_Plex_Mono/OFL.txt
Normal file
93
application/IBM_Plex_Mono/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
application/__pycache__/config.cpython-311.pyc
Normal file
BIN
application/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/database.cpython-311.pyc
Normal file
BIN
application/__pycache__/database.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/main.cpython-311.pyc
Normal file
BIN
application/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/sudoku_generator.cpython-311.pyc
Normal file
BIN
application/__pycache__/sudoku_generator.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/thermal_print.cpython-311.pyc
Normal file
BIN
application/__pycache__/thermal_print.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/utils.cpython-311.pyc
Normal file
BIN
application/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
application/__pycache__/wordsearch_generator.cpython-311.pyc
Normal file
BIN
application/__pycache__/wordsearch_generator.cpython-311.pyc
Normal file
Binary file not shown.
18
application/config.py
Normal file
18
application/config.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
auth0_domain: str
|
||||||
|
auth0_api_audience: str
|
||||||
|
auth0_issuer: str
|
||||||
|
auth0_algorithms: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_settings():
|
||||||
|
return Settings()
|
1
application/data.json
Normal file
1
application/data.json
Normal file
File diff suppressed because one or more lines are too long
73
application/database.py
Normal file
73
application/database.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
# from datetime import datetime
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
class TodoDatabase:
|
||||||
|
def __init__(self, filename="data.json"):
|
||||||
|
self.filename = filename
|
||||||
|
self.database = {}
|
||||||
|
try:
|
||||||
|
with open(self.filename) as f:
|
||||||
|
self.database = json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save_todo(self, date, todo, save=True):
|
||||||
|
if date not in self.database:
|
||||||
|
# if not len(self.database[date]) > 0:
|
||||||
|
self.database[date] = []
|
||||||
|
if todo.recurring:
|
||||||
|
self.database["recurring"].append(todo.model_dump())
|
||||||
|
self.database[date].append(todo.model_dump())
|
||||||
|
|
||||||
|
if save:
|
||||||
|
self._save_database()
|
||||||
|
|
||||||
|
def save_todos(self, date, todos):
|
||||||
|
for todo in todos:
|
||||||
|
self.save_todo(date, todo, save=False)
|
||||||
|
self._save_database()
|
||||||
|
|
||||||
|
def remove_todos(self, date):
|
||||||
|
if date in self.database:
|
||||||
|
del self.database[date]
|
||||||
|
|
||||||
|
def read_todos(self, date):
|
||||||
|
# if self.datebase[date] is None:
|
||||||
|
# return []
|
||||||
|
if date not in self.database:
|
||||||
|
return []
|
||||||
|
return self.database[date]
|
||||||
|
|
||||||
|
def _update_quotes(self):
|
||||||
|
r = requests.get("https://zenquotes.io/api/quotes")
|
||||||
|
res = r.json()
|
||||||
|
|
||||||
|
# if "quotes" not in self.database:
|
||||||
|
self.database["quotes"] = res
|
||||||
|
self.database["quotes_last_updated"] = time.time()
|
||||||
|
self._save_database()
|
||||||
|
|
||||||
|
def get_random_quote(self):
|
||||||
|
# print(time.time())
|
||||||
|
if time.time()-self.database["quotes_last_updated"] > 86400:
|
||||||
|
self._update_quotes()
|
||||||
|
quote = random.choice(self.database["quotes"])
|
||||||
|
|
||||||
|
return [quote['q'], "- " + quote['a']]
|
||||||
|
|
||||||
|
|
||||||
|
def _save_database(self):
|
||||||
|
with open(self.filename, 'w') as f:
|
||||||
|
json.dump(self.database, f)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
data = TodoDatabase()
|
||||||
|
# data._update_quotes()
|
||||||
|
print(data.get_random_quote())
|
||||||
|
|
||||||
|
|
||||||
|
|
148
application/main.py
Normal file
148
application/main.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""Python FastAPI Auth0 integration example
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Security
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from .utils import VerifyToken
|
||||||
|
from .database import TodoDatabase
|
||||||
|
from .thermal_print import ThermalPrinter
|
||||||
|
|
||||||
|
# from wsgiref import simple_server
|
||||||
|
|
||||||
|
# Creates app instance
|
||||||
|
app = FastAPI()
|
||||||
|
auth = VerifyToken()
|
||||||
|
data = TodoDatabase()
|
||||||
|
# printer = ThermalPrinter()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Todo(BaseModel):
|
||||||
|
time: str
|
||||||
|
task: str
|
||||||
|
recurring: bool
|
||||||
|
|
||||||
|
class TodoList(BaseModel):
|
||||||
|
date: str #Always current date OLD TEXT: #Either current date in %Y-%m-%d format or "recurring" for a recurring task
|
||||||
|
todos: list[Todo]
|
||||||
|
|
||||||
|
|
||||||
|
class TodoDate(BaseModel):
|
||||||
|
date: str
|
||||||
|
# date: str = datetime.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
|
# @app.get("/api/public")
|
||||||
|
# def public():
|
||||||
|
# """No access token required to access this route"""
|
||||||
|
|
||||||
|
# result = {
|
||||||
|
# "status": "success",
|
||||||
|
# "msg": ("Hello from a public endpoint! You don't need to be "
|
||||||
|
# "authenticated to see this.")
|
||||||
|
# }
|
||||||
|
# return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/todos/get")
|
||||||
|
def get_todos(date: str = datetime.today().strftime('%Y-%m-%d'), auth_result: str = Security(auth.verify)):
|
||||||
|
"""A valid access token is required to access this route"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"todos": data.read_todos(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
# result = {
|
||||||
|
# "todos": [
|
||||||
|
# {"time": "1:00pm to 2:00pm", "text": "Read a book"},
|
||||||
|
# {"time": "2:00pm to 3:00pm", "text": "Go for a walk"}
|
||||||
|
# ]
|
||||||
|
# } #TODO: replace with database access
|
||||||
|
|
||||||
|
# return result
|
||||||
|
|
||||||
|
|
||||||
|
# return auth_result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/todos/write")
|
||||||
|
def write_todos(todos: TodoList, auth_result: str = Security(auth.verify)):
|
||||||
|
"""A valid access token is required to access this route"""
|
||||||
|
|
||||||
|
# result = {
|
||||||
|
# "todos": [
|
||||||
|
# {"time": "1:00pm to 2:00pm", "text": "Read a book"},
|
||||||
|
# {"time": "2:00pm to 3:00pm", "text": "Go for a walk"}
|
||||||
|
# ]
|
||||||
|
# } #TODO: replace with database write
|
||||||
|
|
||||||
|
# return result
|
||||||
|
|
||||||
|
# data.save_todo()
|
||||||
|
|
||||||
|
print(todos)
|
||||||
|
|
||||||
|
# if todos.date != "recurring":
|
||||||
|
# data.remove_todos(todos.date)
|
||||||
|
data.remove_todos(todos.date)
|
||||||
|
data.save_todos(todos.date, todos.todos)
|
||||||
|
|
||||||
|
return {"result": todos.date}
|
||||||
|
|
||||||
|
# return todos
|
||||||
|
|
||||||
|
|
||||||
|
# @app.post("/api/todos/write_recurring")
|
||||||
|
# def write_todos_recurring(todos: TodoList, auth_result: str = Security(auth.verify)):
|
||||||
|
|
||||||
|
# data.save_todos("recurring", todos.todos)
|
||||||
|
|
||||||
|
# return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/todos/print")
|
||||||
|
def print_todos(date: str = datetime.today().strftime('%Y-%m-%d'), auth_result: str = Security(auth.verify)):
|
||||||
|
"""A valid access token is required to access this route"""
|
||||||
|
|
||||||
|
# result = {
|
||||||
|
# "todos": [
|
||||||
|
# {"time": "1:00pm to 2:00pm", "text": "Read a book"},
|
||||||
|
# {"time": "2:00pm to 3:00pm", "text": "Go for a walk"}
|
||||||
|
# ]
|
||||||
|
# } #TODO: replace with access to database if no todos provided from request, otherwise print request data
|
||||||
|
|
||||||
|
todos = data.get_todos(date)
|
||||||
|
printer.print_todos()
|
||||||
|
|
||||||
|
return {"result": "success"}
|
||||||
|
|
||||||
|
# class SaveTodos:
|
||||||
|
# def on_post(self, req, resp):
|
||||||
|
# pass #Handle web interface sending updated todo list
|
||||||
|
|
||||||
|
# class PrintTodos:
|
||||||
|
# def on_post(self, req, resp):
|
||||||
|
# pass #Handle web interface requesting a print action
|
||||||
|
|
||||||
|
|
||||||
|
# @app.get("/api/get_todos-scoped")
|
||||||
|
# def private_scoped(auth_result: str = Security(auth.verify, scopes=['todos:all'])):
|
||||||
|
# """A valid access token and an appropriate scope are required to access
|
||||||
|
# this route
|
||||||
|
# """
|
||||||
|
|
||||||
|
# return auth_result
|
||||||
|
|
||||||
|
|
||||||
|
app.mount("/", StaticFiles(directory="application/static",html = True), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
# if __name__ == '__main__':
|
||||||
|
# print('ay')
|
||||||
|
# httpd = simple_server.make_server('127.0.0.1', 8000, app)
|
||||||
|
# httpd.serve_forever()
|
311
application/static/index.html
Normal file
311
application/static/index.html
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Test</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="loginPanel" class="flex flex-row items-center justify-center fixed w-full h-full overflow-y-hidden overflow-x-hidden bg-white">
|
||||||
|
|
||||||
|
<button id='loginButton' class="p-2 pl-8 pr-8 bg-purple-500 rounded-lg text-white text-xl font-semibold">Login</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div id="testToken"></div> -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- nice colour: bg-gray-800 -->
|
||||||
|
<div id="timePickerWindow" class="absolute w-full h-full overflow-y-hidden top-0 left-0 hidden">
|
||||||
|
<div class="flex flex-row w-full justify-center items-center text-center h-screen">
|
||||||
|
<div class="absolute w-full h-full bg-slate-800 opacity-40 top-0 left-0"></div>
|
||||||
|
<div class="bg-white inline-block text-left rounded-lg overflow-hidden align-bottom transition-all transform shadow-2xl sm:my-8 sm:align-middle sm:max-w-xl sm:w-full">
|
||||||
|
<div class="flex flex-col items-center pt-6 pr-6 pb-6 pl-6">
|
||||||
|
<p class="text-2xl font-semibold leading-none tracking-tighter lg:text-3xl">When?</p>
|
||||||
|
<!-- <p class="mt-3 text-base leading-relaxed text-center">I am a fullstack software developer with ReactJS for frontend and NodeJS for backend</p> -->
|
||||||
|
<!-- <div class="rese"> -->
|
||||||
|
<div id="radios">
|
||||||
|
<input id="rad1" type="radio" name="radioBtn" checked>
|
||||||
|
<label class="labels" for="rad1">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="font-semibold">Start</span>
|
||||||
|
<span id="startTimeLabel">12:59 am</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input id="rad2" type="radio" name="radioBtn">
|
||||||
|
<label class="labels" for="rad2">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="font-semibold">End</span>
|
||||||
|
<span id="endTimeLabel">12:59 pm</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input id="rad3" type="radio" name="radioBtn">
|
||||||
|
<!-- <label class="labels" for="rad3">Third Option</label>
|
||||||
|
-->
|
||||||
|
<div id="bckgrnd"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 w-full h-full">
|
||||||
|
<div class="flex flex-row space-x-1 items-center justify-center">
|
||||||
|
<input id="timeInput" class="h-11 rounded-lg p-4" style="background-color:rgba(239,239,240,1);" type="time" name="time" value="12:59">
|
||||||
|
<!-- <input id="hourInput" class="bg-blue-400 z-99 w-11 h-11 text-center text-xl font-semibold" type="number" inputmode="numeric" value="12" />
|
||||||
|
<span class="text-xl font-semibold">:</span>
|
||||||
|
<input id="minuteInput" class="bg-blue-400 z-99 w-11 h-11 text-center text-xl font-semibold" type="number" inputmode="numeric" value="59" />
|
||||||
|
<input id="amPmInput" class="bg-blue-400 z-99 w-11 h-11 text-center text-xl font-semibold" type="text" value="AM" /> -->
|
||||||
|
</div>
|
||||||
|
<div id="quickSelectButtons" class="flex flex-row space-x-1 items-center justify-center flex-wrap">
|
||||||
|
<ul class="donate-now flex flex-col items-center justify-center sm:flex-row space-y-7 sm:space-y-0 sm:space-x-4 mt-12 mb-3">
|
||||||
|
<li>
|
||||||
|
<input class="quickButton" type="radio" id="t15" name="amount" />
|
||||||
|
<label class="cursor-pointer border-2 border-slate-300 p-3 pl-5 pr-5 rounded-lg hover:border-indigo-300" for="t15">15 min</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input class="quickButton" type="radio" id="t30" name="amount" />
|
||||||
|
<label class="cursor-pointer border-2 border-slate-300 p-3 pl-5 pr-5 rounded-lg hover:border-indigo-300" for="t30">30 min</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input class="quickButton" type="radio" id="t60" name="amount" checked="checked" />
|
||||||
|
<label class="cursor-pointer border-2 border-slate-300 p-3 pl-5 pr-5 rounded-lg hover:border-indigo-300" for="t60">1 hour</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input class="quickButton" type="radio" id="t120" name="amount" />
|
||||||
|
<label class="cursor-pointer border-2 border-slate-300 p-3 pl-5 pr-5 rounded-lg hover:border-indigo-300" for="t120">2 hours</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full mt-6">
|
||||||
|
<a id="saveButton" onclick="saveButtonListener()" class="cursor-pointer flex text-center items-center justify-center w-full pt-4 pr-10 pb-4 pl-10 text-base
|
||||||
|
font-medium text-white bg-indigo-600 rounded-xl transition duration-500 ease-in-out transform
|
||||||
|
hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Save</a>
|
||||||
|
<!-- <input class="bg-blue-400 z-99" type="number" inputmode="numeric"/> -->
|
||||||
|
<!-- <input type="number" pattern="[0-9]*" /> -->
|
||||||
|
<!-- <input class="w-full bg-slate-200 absolute top-0 left-0 z-50" type="number" name="test" inputmode="decimal"> -->
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- <input type="number" inputmode="numeric"/> -->
|
||||||
|
<!-- <input type="number" pattern="\d*"/> -->
|
||||||
|
<!-- <input class="fixed bg-slate-200 top-0 left-0 z-50" type="number" name="test" inputmode="decimal"> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="p-2 bg-white shadow-md rounded-lg border m-2" id="table">
|
||||||
|
<!-- <input type="time"> -->
|
||||||
|
<h2 class="font-semibold text-2xl ml-2">Tasks • <span id="date-view">Sat Aug 12, 2023</span></h2>
|
||||||
|
<!-- <h2 class="font-normal text-sm ml-3">Last Synced: <span id="date-view">Sat Aug 12, 2023</span></h2> -->
|
||||||
|
<li class="task-row p-2 m-1 shadow-md border bg-white min-h-11 rounded-lg flex space-x-2 flex-col" data-editor-shown="false" data-recurring="false">
|
||||||
|
<!-- <a class="handle pr-2 text-2xl justify-center items-center content-center">☰</a> -->
|
||||||
|
<div class="flex grow space-x-2 h-full">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 handle cursor-grab shrink-0">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
</svg>
|
||||||
|
<span id="timeslot" class="font-semibold text-sm shrink-0 grow-0 h-fit bg-slate-200 text-slate-600 rounded-md p-0.5 pl-1.5 pr-1.5">12pm</span>
|
||||||
|
<span id="task" class="flex grow">Walk to school while jumping on one leg and scratching your head.</span>
|
||||||
|
</div>
|
||||||
|
<div id="expanded-info" class="flex flex-row space-x-1 items-center hidden mt-6 space-x-3">
|
||||||
|
<div class="timePickerWindowButton hover:bg-slate-200 cursor-pointer rounded-md flex flex-row space-x-1 items-center pr-1">
|
||||||
|
<span class="w-8 h-8 flex space-x-2 space-y-2 items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span id="timeButtonDisplay" class="text-sm font-semibold">12:15pm to 1:15pm</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recurringButton hover:bg-slate-200 cursor-pointer rounded-md flex flex-row space-x-1 items-center">
|
||||||
|
<span class="w-8 h-8 flex space-x-2 space-y-2 items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-black">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<!-- font-semibold text-sm shrink-0 grow-0 h-fit bg-slate-200 text-slate-600 rounded-md p-0.5 pl-1.5 pr-1.5 -->
|
||||||
|
<div class="p-2 m-1 font-semibold text-slate-600 align-text-top">
|
||||||
|
<div onclick="addTask();" class="cursor-pointer flex flex-row space-x-1 hover:bg-slate-200 w-fit p-2 rounded-lg text-center items-center">
|
||||||
|
<span class="">+</span>
|
||||||
|
<span>Add Task</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
<!-- jsDelivr :: Sortable :: Latest (https://www.jsdelivr.com/package/npm/sortablejs) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var ta = document.getElementById("table")
|
||||||
|
Sortable.create(ta, {
|
||||||
|
handle: '.handle',
|
||||||
|
animation: 150
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<!-- jsDelivr :: Swiper :: Latest (https://www.jsdelivr.com/package/npm/swiper) -->
|
||||||
|
<!-- <script src="https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var defaults = {
|
||||||
|
pagination: '.swiper-pagination',
|
||||||
|
slidesPerView: 3,
|
||||||
|
freeMode: true,
|
||||||
|
freeModeSticky: false,
|
||||||
|
freeModeMomentumRatio: 0.25,
|
||||||
|
freeModeVelocityRatio: 0.25,
|
||||||
|
freeModeMinimumVelocity: 0.1,
|
||||||
|
mousewheelControl: true,
|
||||||
|
mousewheelSensitivity: 0.5,
|
||||||
|
loop: false,
|
||||||
|
loopAdditionalSlides: 5,
|
||||||
|
direction: 'vertical',
|
||||||
|
slideToClickedSlide: true,
|
||||||
|
centeredSlides: true
|
||||||
|
};
|
||||||
|
|
||||||
|
new Swiper(
|
||||||
|
'.swiper-container.hours',
|
||||||
|
Object.assign({}, defaults, { initialSlide: 13})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Swiper(
|
||||||
|
'.swiper-container.minutes',
|
||||||
|
Object.assign({}, defaults, { initialSlide: 37})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Swiper('.swiper-container.seconds', defaults);
|
||||||
|
</script> -->
|
||||||
|
<!-- jsDelivr :: Day.js :: Latest (https://www.jsdelivr.com/package/npm/dayjs) -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/customParseFormat.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
dayjs.extend(window.dayjs_plugin_customParseFormat)
|
||||||
|
|
||||||
|
let now = dayjs()
|
||||||
|
document.getElementById("startTimeLabel").innerHTML = now.format("h:mm a")
|
||||||
|
document.getElementById("endTimeLabel").innerHTML = now.add(1, 'h').format("h:mm a")
|
||||||
|
document.getElementById("timeInput").value = dayjs().format("HH:mm")
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(dayjs().format("HH:mm"))
|
||||||
|
// console.log(dayjs().add(1, 'h').format("HH:mm"))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// $('#getTokenPopup').click(async () => {
|
||||||
|
// const token = await auth0.getTokenWithPopup({
|
||||||
|
// authorizationParams: {
|
||||||
|
// audience: 'https://mydomain/api/',
|
||||||
|
// scope: 'read:rules'
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// import { createAuth0Client } from '@auth0/auth0-spa-js';
|
||||||
|
let jToken = ""
|
||||||
|
|
||||||
|
function loginAction(e) {
|
||||||
|
auth0.createAuth0Client({
|
||||||
|
domain: 'dev-kazsp1tz0e7t5d07.us.auth0.com',
|
||||||
|
clientId: 'Pw2MvNmIAJUA4THZzsZTEeqkXnCTHYr3'
|
||||||
|
}).then(a0 => {
|
||||||
|
const token = a0.getTokenWithPopup({
|
||||||
|
authorizationParams: {
|
||||||
|
audience: 'https://RecieptTodos.imsam.ca',
|
||||||
|
scope: 'todos:all'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
token.then(t => {
|
||||||
|
document.getElementById("loginPanel").classList.add("hidden")
|
||||||
|
console.log(t)
|
||||||
|
jToken = t
|
||||||
|
|
||||||
|
// document.getElementById("testToken").innerHTML = t
|
||||||
|
|
||||||
|
fetch('/api/todos/get?' + new URLSearchParams({
|
||||||
|
date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
}), {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Authorization': "Bearer " + t,
|
||||||
|
}
|
||||||
|
}).then(re => {
|
||||||
|
re.json().then(jso => {
|
||||||
|
console.log(jso)
|
||||||
|
for (let todo of jso.todos) {
|
||||||
|
console.log(todo)
|
||||||
|
// function addTask(defaultTimeRange="", defaultTaskText="New Task", editable=true) {
|
||||||
|
addTask(defaultTimeRange=todo.time, defaultTaskText=todo.text, editable=false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// var items = JSON.parse(re.json())
|
||||||
|
// console.log(items.json)
|
||||||
|
})
|
||||||
|
|
||||||
|
//TODO: Make call with token to backend to load all current todos
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("loginButton").addEventListener('click', loginAction)
|
||||||
|
// document.getElementById("loginButton").addEventListener('click', (e) => {
|
||||||
|
// auth0.createAuth0Client({
|
||||||
|
// domain: 'dev-kazsp1tz0e7t5d07.us.auth0.com',
|
||||||
|
// clientId: 'Pw2MvNmIAJUA4THZzsZTEeqkXnCTHYr3'
|
||||||
|
// }).then(a0 => {
|
||||||
|
// const token = a0.getTokenWithPopup({
|
||||||
|
// authorizationParams: {
|
||||||
|
// audience: 'https://RecieptTodos.imsam.ca',
|
||||||
|
// scope: 'todos:all'
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// token.then(t => {
|
||||||
|
// document.getElementById("loginPanel").classList.add("hidden")
|
||||||
|
// console.log(t)
|
||||||
|
// jToken = t
|
||||||
|
|
||||||
|
// // document.getElementById("testToken").innerHTML = t
|
||||||
|
|
||||||
|
// fetch('/api/todos/get?' + new URLSearchParams({
|
||||||
|
// date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
// }), {
|
||||||
|
// method: 'GET',
|
||||||
|
// withCredentials: true,
|
||||||
|
// credentials: 'include',
|
||||||
|
// headers: {
|
||||||
|
// 'Authorization': "Bearer " + t,
|
||||||
|
// }
|
||||||
|
// }).then(re => {
|
||||||
|
// re.json().then(jso => {
|
||||||
|
// console.log(jso)
|
||||||
|
// for (let todo of jso.todos) {
|
||||||
|
// console.log(todo)
|
||||||
|
// // function addTask(defaultTimeRange="", defaultTaskText="New Task", editable=true) {
|
||||||
|
// addTask(defaultTimeRange=todo.time, defaultTaskText=todo.text, editable=false)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// // var items = JSON.parse(re.json())
|
||||||
|
// // console.log(items.json)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// //TODO: Make call with token to backend to load all current todos
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="scripts.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
601
application/static/scripts.js
Normal file
601
application/static/scripts.js
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
// <script type="text/javascript">
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
<li class="task-row p-2 m-1 shadow-md border bg-white rounded-lg flex items-center space-x-1" data-editor-shown="false">
|
||||||
|
<!-- <a class="handle pr-2 text-2xl justify-center items-center content-center">☰</a> -->
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 handle mr-2 cursor-grab shrink-0">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
<span id="timeslot" class="font-semibold shrink-0">11:00-12:30 • </span>
|
||||||
|
<span id="task" class="flex grow shrink">Row 4</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
function singleClickListener(e) {
|
||||||
|
// console.log(e)
|
||||||
|
// e.target.style.height += 100;
|
||||||
|
// e.target.classList.remove("h-11")
|
||||||
|
// e.target.classList.add("h-24")
|
||||||
|
|
||||||
|
console.log(e.target.tagName)
|
||||||
|
console.log(e.target.parentElement.tagName)
|
||||||
|
|
||||||
|
|
||||||
|
if (e.target.tagName !== "LI" && e.target.parentElement.tagName !== "LI" && e.target.parentElement.parentElement.tagName !== "LI") return;
|
||||||
|
|
||||||
|
if (e.target.tagName === "SPAN" && e.target.contentEditable === "true") return;
|
||||||
|
|
||||||
|
|
||||||
|
let activeEle = e.target
|
||||||
|
if (e.target.parentElement.tagName === "LI") {
|
||||||
|
activeEle = e.target.parentElement
|
||||||
|
}
|
||||||
|
if (e.target.parentElement.parentElement.tagName == "LI") {
|
||||||
|
activeEle = e.target.parentElement.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// var height = activeEle.offsetHeight;
|
||||||
|
|
||||||
|
|
||||||
|
if (activeEle.dataset.expandedView === 'true') {
|
||||||
|
// var newHeight = height - 75;
|
||||||
|
activeEle.style.height = null;
|
||||||
|
activeEle.dataset.expandedView = false
|
||||||
|
activeEle.querySelector("#task").contentEditable = "false"
|
||||||
|
activeEle.querySelector("#expanded-info").classList.add("hidden")
|
||||||
|
|
||||||
|
// activeEle.querySelector("#task").blur()
|
||||||
|
// activeEle.classList.add("h-44")
|
||||||
|
// activeEle.classList.remove("h-44")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// var newHeight = height + 75;
|
||||||
|
activeEle.style.height = (activeEle.offsetHeight + 75) + 'px';
|
||||||
|
activeEle.dataset.expandedView = true
|
||||||
|
activeEle.querySelector("#task").contentEditable = "true"
|
||||||
|
activeEle.querySelector("#task").focus()
|
||||||
|
selectText(activeEle.querySelector("#task"))
|
||||||
|
activeEle.querySelector("#expanded-info").classList.remove("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(newHeight)
|
||||||
|
// activeEle.style.height = newHeight + 'px';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// let focOutFired = false
|
||||||
|
let triggerFocOutEvent = true
|
||||||
|
|
||||||
|
function onClickListener(e) {
|
||||||
|
// if (!focOutFired) return;
|
||||||
|
|
||||||
|
// focOutFired = false
|
||||||
|
triggerFocOutEvent = false
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function blurOrKeypress(e) {
|
||||||
|
// split up largely for readability
|
||||||
|
console.log(e)
|
||||||
|
// console.log(document.activeElement)
|
||||||
|
if (!triggerFocOutEvent) {
|
||||||
|
triggerFocOutEvent = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target.tagName !== "SPAN" && e.target.id !== "task") return false;
|
||||||
|
if ((e.type === 'keypress' && e.code != 'Enter' && !e.ctrlKey) || e.target.parentElement.parentElement.dataset.expandedView !== "true") return false;
|
||||||
|
|
||||||
|
// if (e.type === "focusout") focOutFired = true;
|
||||||
|
|
||||||
|
// if () {}
|
||||||
|
|
||||||
|
// if (e.target.parentElement.parentElement.dataset.expandedView === "true") {
|
||||||
|
// e.target.contentEditable = "false"
|
||||||
|
// e.target.parentElement.parentElement.style.height = null;
|
||||||
|
// e.target.parentElement.parentElement.dataset.expandedView = false
|
||||||
|
// e.target.parentElement.parentElement.querySelector("#expanded-info").classList.add("hidden")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (e.type === 'keypress' && (e.code == 'Enter' || e.ctrlKey)) {
|
||||||
|
e.target.contentEditable = "false"
|
||||||
|
e.target.blur()
|
||||||
|
e.target.parentElement.parentElement.style.height = null;
|
||||||
|
e.target.parentElement.parentElement.dataset.expandedView = false
|
||||||
|
e.target.parentElement.parentElement.querySelector("#expanded-info").classList.add("hidden")
|
||||||
|
|
||||||
|
|
||||||
|
if (e.target.innerHTML === "") {
|
||||||
|
e.target.parentElement.parentElement.parentElement.removeChild(e.target.parentElement.parentElement)
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// if (e.target.tagName !== 'INPUT') return false;
|
||||||
|
// if (e.type === 'keypress' && e.code != 'Enter' && !e.ctrlKey) return false;
|
||||||
|
|
||||||
|
// // a parent, a row, and a newly minted text node walk into a bar...
|
||||||
|
// const parent = e.target.parentElement;
|
||||||
|
// const row = parent.parentElement;
|
||||||
|
// const text = document.createTextNode(e.target.value || e.target.placeholder);
|
||||||
|
// /* .isConnected refers to it's state in the DOM. this was some work to try and stop an error that was
|
||||||
|
// ocurring due to this being simultaneously the 'blur' 'keypress' event handler. Alas, it didn't.
|
||||||
|
// If the error is really an issue, then wrapping the parent.replaceChild in a try/catch block should solve it for you.*/
|
||||||
|
// if (e.target.isConnected) {
|
||||||
|
// // use the dataset key + the textarea's value to update the definitions.
|
||||||
|
// // definitions[row.dataset.key] = e.target.value;
|
||||||
|
// // write those to the local storage
|
||||||
|
// // localStorage.setItem('definitions', JSON.stringify(definitions));
|
||||||
|
|
||||||
|
// // Or, if you are using a database, you would use some variety of AJAX/XHR call here.
|
||||||
|
|
||||||
|
// // get rid of our text element
|
||||||
|
// parent.replaceChild(text, e.target);
|
||||||
|
// // reset the editorshown value in case we need to update this again
|
||||||
|
// row.dataset.editorShown = false;
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// <li class="task-row p-2 m-1 shadow-md border bg-white min-h-11 rounded-lg flex space-x-2 flex-col" data-editor-shown="false">
|
||||||
|
// <!-- <a class="handle pr-2 text-2xl justify-center items-center content-center">☰</a> -->
|
||||||
|
// <div class="flex grow space-x-2 h-full">
|
||||||
|
// <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 handle cursor-grab shrink-0">
|
||||||
|
// <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
// </svg>
|
||||||
|
|
||||||
|
|
||||||
|
// <span id="timeslot" class="font-semibold text-sm shrink-0 grow-0 h-fit bg-slate-200 text-slate-600 rounded-md p-0.5 pl-1.5 pr-1.5">1pm</span>
|
||||||
|
// <span id="task" class="flex grow">Walk to school while jumping on one leg and scratching your head.</span>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div id="expanded-info" class="flex flex-row space-x-1 items-center hidden mt-6">
|
||||||
|
// <div class="timePickerWindowButton hover:bg-slate-200 cursor-pointer rounded-md flex flex-row space-x-1 items-center pr-1">
|
||||||
|
// <span class="w-8 h-8 flex space-x-2 space-y-2 items-center justify-center">
|
||||||
|
// <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
// <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
// </svg>
|
||||||
|
// </span>
|
||||||
|
// <span id="timeButtonDisplay" class="text-sm font-semibold">12:15pm to 1:15pm</span>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// </div>
|
||||||
|
// </li>
|
||||||
|
|
||||||
|
function selectText(node) {
|
||||||
|
/* Helper function slightly modified from: https://stackoverflow.com/a/987376 */
|
||||||
|
|
||||||
|
// const node = document.getElementById(nodeId);
|
||||||
|
|
||||||
|
if (document.body.createTextRange) {
|
||||||
|
const range = document.body.createTextRange();
|
||||||
|
range.moveToElementText(node);
|
||||||
|
range.select();
|
||||||
|
} else if (window.getSelection) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
} else {
|
||||||
|
console.warn("Could not select text in node: Unsupported browser.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addTask(defaultTimeRange="", defaultTaskText="New Task", editable=true) {
|
||||||
|
/* defaultTimeRange must be in format h:mma to h:mma (e.g: 1:15pm to 2:15pm) */
|
||||||
|
let rows = document.getElementById("table")
|
||||||
|
|
||||||
|
const row = document.createElement("li")
|
||||||
|
row.dataset.editorShown = false
|
||||||
|
row.dataset.recurring = false
|
||||||
|
row.className = "task-row p-2 m-1 shadow-md border bg-white min-h-11 rounded-lg flex space-x-2 flex-col"
|
||||||
|
|
||||||
|
let firstContainer = document.createElement("div")
|
||||||
|
firstContainer.className = "flex grow space-x-2 h-full"
|
||||||
|
firstContainer.innerHTML += "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6 handle cursor-grab shrink-0'><path stroke-linecap='round' stroke-linejoin='round' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5' /></svg>"
|
||||||
|
|
||||||
|
let timeslotSpan = document.createElement("span")
|
||||||
|
timeslotSpan.id = "timeslot"
|
||||||
|
timeslotSpan.className = "font-semibold text-sm shrink-0 grow-0 h-fit bg-slate-200 text-slate-600 rounded-md p-0.5 pl-1.5 pr-1.5 hidden"
|
||||||
|
if (defaultTimeRange !== "") {
|
||||||
|
timeslotSpan.classList.remove("hidden")
|
||||||
|
let parsedTime = dayjs(defaultTimeRange.split(' to ')[0], 'h:mma')
|
||||||
|
|
||||||
|
timeslotSpan.innerHTML = parsedTime.format("h:mma")
|
||||||
|
if (parsedTime.minute() == 0) {
|
||||||
|
timeslotSpan.innerHTML = parsedTime.format("ha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstContainer.appendChild(timeslotSpan)
|
||||||
|
|
||||||
|
let taskSpan = document.createElement("span")
|
||||||
|
taskSpan.id = "task"
|
||||||
|
taskSpan.className = "flex grow"
|
||||||
|
taskSpan.innerHTML = defaultTaskText
|
||||||
|
taskSpan.contentEditable = editable
|
||||||
|
firstContainer.appendChild(taskSpan)
|
||||||
|
|
||||||
|
row.appendChild(firstContainer)
|
||||||
|
|
||||||
|
|
||||||
|
let secondContainer = document.createElement("div")
|
||||||
|
secondContainer.id = "expanded-info"
|
||||||
|
secondContainer.className = "flex flex-row space-x-1 items-center mt-6 hidden"
|
||||||
|
|
||||||
|
let timePickerWindowButton = document.createElement("div")
|
||||||
|
timePickerWindowButton.className = "timePickerWindowButton hover:bg-slate-200 cursor-pointer rounded-md flex flex-row space-x-1 items-center pr-1"
|
||||||
|
secondContainer.appendChild(timePickerWindowButton)
|
||||||
|
|
||||||
|
let clockSpan = document.createElement("span")
|
||||||
|
clockSpan.className = "w-8 h-8 flex space-x-2 space-y-2 items-center justify-center"
|
||||||
|
clockSpan.innerHTML += "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6'><path stroke-linecap='round' stroke-linejoin='round' d='M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z' /></svg>"
|
||||||
|
timePickerWindowButton.appendChild(clockSpan)
|
||||||
|
|
||||||
|
let timeButtonDisplay = document.createElement("span")
|
||||||
|
timeButtonDisplay.id = "timeButtonDisplay"
|
||||||
|
timeButtonDisplay.className = "text-sm font-semibold"
|
||||||
|
timeButtonDisplay.innerHTML = defaultTimeRange
|
||||||
|
timePickerWindowButton.appendChild(timeButtonDisplay)
|
||||||
|
|
||||||
|
row.appendChild(secondContainer)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// row.innerHTML += "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6 handle mr-2 cursor-grab shrink-0'><path stroke-linecap='round' stroke-linejoin='round' d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'/></svg>"
|
||||||
|
|
||||||
|
// const t = document.createElement("span")
|
||||||
|
// t.id = "timeslot"
|
||||||
|
// t.className = "font-semibold shrink-0"
|
||||||
|
// t.innerHTML = "xx:xx-xx:xx • "
|
||||||
|
// const ed = document.createElement("span")
|
||||||
|
// ed.id = "task"
|
||||||
|
// ed.className = "flex grow shrink"
|
||||||
|
// ed.innerHTML = "New Task"
|
||||||
|
|
||||||
|
// row.appendChild(t)
|
||||||
|
// row.appendChild(ed)
|
||||||
|
|
||||||
|
// const ta = document.createElement('input')
|
||||||
|
// ta.classList.add('grow')
|
||||||
|
// ta.placeholder = ed.innerHTML
|
||||||
|
// ed.replaceChild(ta, ed.firstChild)
|
||||||
|
|
||||||
|
rows.insertBefore(row, rows.lastElementChild)
|
||||||
|
|
||||||
|
|
||||||
|
if (editable) {
|
||||||
|
row.style.height = (row.offsetHeight + 75) + 'px';
|
||||||
|
row.dataset.expandedView = true
|
||||||
|
// row.querySelector("#task").contentEditable = "true"
|
||||||
|
// row.querySelector("#task").focus()
|
||||||
|
taskSpan.focus()
|
||||||
|
selectText(taskSpan)
|
||||||
|
secondContainer.classList.remove("hidden")
|
||||||
|
// row.querySelector("#expanded-info").classList.remove("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
row.addEventListener('keypress', blurOrKeypress);
|
||||||
|
row.addEventListener('focusout', blurOrKeypress);
|
||||||
|
secondContainer.addEventListener('mousedown', onClickListener)
|
||||||
|
timePickerWindowButton.addEventListener("click", showTimePickerWindow)
|
||||||
|
timeslotSpan.addEventListener("mousedown", timeSlotShowTimePickerButton)
|
||||||
|
row.addEventListener('click', singleClickListener);
|
||||||
|
|
||||||
|
// ed.firstChild.focus()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function dblListener(e) {
|
||||||
|
|
||||||
|
let tarEle = e.target
|
||||||
|
const parent = tarEle.parentElement;
|
||||||
|
|
||||||
|
if (tarEle.tagName === "LI" && tarEle.dataset.editorShown !== 'true') {
|
||||||
|
const ed = tarEle.querySelector('span:last-child')
|
||||||
|
const ta = document.createElement('input')
|
||||||
|
ta.classList.add('grow')
|
||||||
|
ta.placeholder = ed.innerHTML
|
||||||
|
ed.replaceChild(ta, ed.firstChild)
|
||||||
|
ed.firstChild.focus()
|
||||||
|
tarEle.dataset.editorShown = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.tagName !== 'LI' || parent.dataset.editorShown === 'true' || tarEle.dataset.editorShown === 'true') return;
|
||||||
|
|
||||||
|
|
||||||
|
const ed = parent.querySelector('span:last-child')
|
||||||
|
const ta = document.createElement('input')
|
||||||
|
ta.classList.add('grow')
|
||||||
|
// ta.classList.add('w-1/2')
|
||||||
|
ta.placeholder = ed.innerHTML
|
||||||
|
ed.replaceChild(ta, ed.firstChild)
|
||||||
|
ed.firstChild.focus()
|
||||||
|
parent.dataset.editorShown = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function timeListener(e) {
|
||||||
|
console.log(e.target.value)
|
||||||
|
let parsedDate = dayjs(e.target.value, 'HH:mm')
|
||||||
|
let activeZone = "startTimeLabel"
|
||||||
|
|
||||||
|
if (document.getElementById("rad2").checked) {
|
||||||
|
activeZone = "endTimeLabel"
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(activeZone).innerHTML = parsedDate.format('h:mm a')
|
||||||
|
// document.getElementById("endTimeLabel").innerHTML = parsedDate.add().format('h:mm a')
|
||||||
|
|
||||||
|
// console.log(dayjs(e.target.value, 'HH:mm').add(1, 'h').format("HH:mm"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function endTimeListener(e) {
|
||||||
|
if (!e.target.checked) return;
|
||||||
|
|
||||||
|
document.getElementById("quickSelectButtons").classList.add("hidden")
|
||||||
|
document.getElementById("timeInput").value = dayjs(document.getElementById("endTimeLabel").innerHTML, 'h:mm a').format('HH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimeListener(e, triggeredFromCode=false) {
|
||||||
|
if (!triggeredFromCode) {
|
||||||
|
if (!e.target.checked) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("quickSelectButtons").classList.remove("hidden")
|
||||||
|
document.getElementById("timeInput").value = dayjs(document.getElementById("startTimeLabel").innerHTML, 'h:mm a').format('HH:mm')
|
||||||
|
let startTime = dayjs(document.getElementById("startTimeLabel").innerHTML, 'h:mm a')
|
||||||
|
let endTime = dayjs(document.getElementById("endTimeLabel").innerHTML, 'h:mm a')
|
||||||
|
let idToAmount = {
|
||||||
|
"t15": 15,
|
||||||
|
"t30": 30,
|
||||||
|
"t60": 60,
|
||||||
|
"t120": 120
|
||||||
|
}
|
||||||
|
for (let button of document.getElementsByClassName("quickButton")) {
|
||||||
|
if (endTime.isSame(startTime.add(idToAmount[button.id], 'm'))) {
|
||||||
|
button.checked = 'checked'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
button.checked = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function quickButtonListener(e) {
|
||||||
|
if (!e.target.checked) return;
|
||||||
|
let idToAmount = {
|
||||||
|
"t15": 15,
|
||||||
|
"t30": 30,
|
||||||
|
"t60": 60,
|
||||||
|
"t120": 120
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(e.target.id)
|
||||||
|
console.log(document.getElementById("startTimeLabel").innerHTML)
|
||||||
|
console.log(dayjs(document.getElementById("startTimeLabel").innerHTML, "h:mm a"))
|
||||||
|
document.getElementById("endTimeLabel").innerHTML = dayjs(document.getElementById("startTimeLabel").innerHTML, "h:mm a").add(idToAmount[e.target.id], 'm').format('h:mm a')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function saveButtonListener(e) {
|
||||||
|
let startTime = dayjs(document.getElementById("startTimeLabel").innerHTML, 'h:mm a')
|
||||||
|
let endTime = dayjs(document.getElementById("endTimeLabel").innerHTML, 'h:mm a')
|
||||||
|
console.log('??')
|
||||||
|
console.log(document.getElementsByClassName("timePickerWindowButton"))
|
||||||
|
|
||||||
|
startTimeListener("", triggeredFromCode=true)
|
||||||
|
document.getElementById("rad1").checked = "clicked"
|
||||||
|
document.getElementById("rad2").checked = null
|
||||||
|
document
|
||||||
|
|
||||||
|
for (let b of document.getElementsByClassName("timePickerWindowButton")) {
|
||||||
|
// let b = bu.querySelector("#timeButtonDisplay")
|
||||||
|
console.log('==')
|
||||||
|
console.log(b)
|
||||||
|
console.log(b.dataset.timepickershown)
|
||||||
|
console.log('===')
|
||||||
|
|
||||||
|
|
||||||
|
if (b.dataset.timepickershown === 'true') {
|
||||||
|
b.dataset.timepickershown = false
|
||||||
|
b.querySelector("#timeButtonDisplay").innerHTML = startTime.format('h:mma') + " to " + endTime.format('h:mma')
|
||||||
|
b.parentElement.parentElement.querySelector("#timeslot").innerHTML = startTime.format('ha')
|
||||||
|
console.log(startTime.minute())
|
||||||
|
if (startTime.minute() != 0) {
|
||||||
|
b.parentElement.parentElement.querySelector("#timeslot").innerHTML = startTime.format('h:mma')
|
||||||
|
}
|
||||||
|
b.parentElement.parentElement.querySelector("#timeslot").classList.remove("hidden")
|
||||||
|
document.getElementById("timePickerWindow").classList.add("hidden")
|
||||||
|
|
||||||
|
|
||||||
|
b.parentElement.parentElement.querySelector("#task").contentEditable = "false"
|
||||||
|
b.parentElement.parentElement.querySelector("#task").blur()
|
||||||
|
b.parentElement.parentElement.style.height = null;
|
||||||
|
b.parentElement.parentElement.dataset.expandedView = false
|
||||||
|
b.parentElement.parentElement.querySelector("#expanded-info").classList.add("hidden")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// b.addEventListener("click", showTimePickerWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(startTime.format('ha'))
|
||||||
|
// console.log(startTime.format('h:mma') + " to " + endTime.format('h:mma'))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// document.getElementById("timePickerWindowButton")
|
||||||
|
function showTimePickerWindow(e) {
|
||||||
|
// console.log(e.target)
|
||||||
|
// console.log(e.target.dataset.timepickershown)
|
||||||
|
// if (e.target.dataset.timepickershown === 'true') return;
|
||||||
|
|
||||||
|
// e.target.dataset.timepickershown = true
|
||||||
|
|
||||||
|
console.log(e.target)
|
||||||
|
console.log(e.target.dataset.timepickershown)
|
||||||
|
if (this.dataset.timepickershown === 'true') return;
|
||||||
|
|
||||||
|
this.dataset.timepickershown = true
|
||||||
|
|
||||||
|
// document.getElementById("")
|
||||||
|
let times = this.querySelector("#timeButtonDisplay").innerHTML.split(" to ")
|
||||||
|
if (times[0] == "") {
|
||||||
|
times[0] = dayjs().format("h:mma")
|
||||||
|
times[1] = dayjs().add(1, 'h').format("h:mma")
|
||||||
|
}
|
||||||
|
console.log(times)
|
||||||
|
// console.log(startTime)
|
||||||
|
// console.log(endTime)
|
||||||
|
|
||||||
|
document.getElementById("startTimeLabel").innerHTML = dayjs(times[0], 'h:mma').format("h:mm a")
|
||||||
|
document.getElementById("endTimeLabel").innerHTML = dayjs(times[1], 'h:mma').format("h:mm a")
|
||||||
|
|
||||||
|
document.getElementById("timeInput").value = dayjs(times[0], 'h:mma').format("HH:mm")
|
||||||
|
document.getElementById("timePickerWindow").classList.remove("hidden")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function timeSlotShowTimePickerButton(e) {
|
||||||
|
console.log('yay!')
|
||||||
|
if (this.parentElement.parentElement.querySelector(".timePickerWindowButton").dataset.timepickershown === 'true') return;
|
||||||
|
console.log('woohoo')
|
||||||
|
this.parentElement.parentElement.querySelector(".timePickerWindowButton").dataset.timepickershown = true
|
||||||
|
|
||||||
|
let times = this.parentElement.parentElement.querySelector(".timePickerWindowButton").querySelector("#timeButtonDisplay").innerHTML.split(" to ")
|
||||||
|
if (times[0] == "") {
|
||||||
|
times[0] = dayjs().format("h:mma")
|
||||||
|
times[1] = dayjs().add(1, 'h').format("h:mma")
|
||||||
|
}
|
||||||
|
console.log(times)
|
||||||
|
// console.log(startTime)
|
||||||
|
// console.log(endTime)
|
||||||
|
|
||||||
|
document.getElementById("startTimeLabel").innerHTML = dayjs(times[0], 'h:mma').format("h:mm a")
|
||||||
|
document.getElementById("endTimeLabel").innerHTML = dayjs(times[1], 'h:mma').format("h:mm a")
|
||||||
|
|
||||||
|
document.getElementById("timeInput").value = dayjs(times[0], 'h:mma').format("HH:mm")
|
||||||
|
document.getElementById("timePickerWindow").classList.remove("hidden")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function recurringButtonListener(e) {
|
||||||
|
|
||||||
|
// TODO: change colour to green
|
||||||
|
//
|
||||||
|
|
||||||
|
if (this.parentElement.parentElement.dataset.recurring === "true") {
|
||||||
|
this.querySelector("svg").classList.add("text-black")
|
||||||
|
this.querySelector("svg").classList.remove("text-lime-600")
|
||||||
|
this.parentElement.parentElement.dataset.recurring = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.querySelector("svg").classList.remove("text-black")
|
||||||
|
this.querySelector("svg").classList.add("text-lime-600")
|
||||||
|
this.parentElement.parentElement.dataset.recurring = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (let rb of document.getElementsByClassName("recurringButton")) {
|
||||||
|
rb.addEventListener("click", recurringButtonListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let rows = document.getElementsByClassName("task-row");
|
||||||
|
console.log('hmm');
|
||||||
|
for (let row of rows) {
|
||||||
|
console.log(row.querySelector("#timeslot"))
|
||||||
|
row.querySelector("#timeslot").addEventListener("mousedown", timeSlotShowTimePickerButton)
|
||||||
|
|
||||||
|
row.addEventListener('keypress', blurOrKeypress);
|
||||||
|
row.addEventListener('focusout', blurOrKeypress);
|
||||||
|
// row.addEventListener('dblclick', dblListener)
|
||||||
|
// row.addEventListener('mousedown', onClickListener)
|
||||||
|
let ei = row.querySelector("#expanded-info")
|
||||||
|
if (ei != null) {
|
||||||
|
ei.addEventListener('mousedown', onClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
row.addEventListener('click', singleClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let button of document.getElementsByClassName("quickButton")) {
|
||||||
|
button.addEventListener("change", quickButtonListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let b of document.getElementsByClassName("timePickerWindowButton")) {
|
||||||
|
b.addEventListener("click", showTimePickerWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("timeInput").addEventListener("blur", timeListener)
|
||||||
|
document.getElementById("rad1").addEventListener("change", startTimeListener)
|
||||||
|
document.getElementById("rad2").addEventListener("change", endTimeListener)
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("date-view").innerHTML = dayjs().format("ddd MMM D, YYYY")
|
||||||
|
|
||||||
|
// document.getElementById("rad1").addEventListener("click", )
|
||||||
|
|
||||||
|
// document.getElementById("timeInput").showPicker()
|
||||||
|
|
||||||
|
// hourInput
|
||||||
|
// minuteInput
|
||||||
|
// amPmInput
|
||||||
|
|
||||||
|
// let hIn = document.getElementById("hourInput")
|
||||||
|
// let mIn = document.getElementById("minuteInput")
|
||||||
|
// let apIn = document.getElementById("amPmInput")
|
||||||
|
|
||||||
|
// hIn.addEventListener("keydown", timeInputListener);
|
||||||
|
// mIn.addEventListener("keydown", timeInputListener);
|
||||||
|
// apIn.addEventListener("keydown", timeInputListener);
|
||||||
|
|
||||||
|
// function timeInputListener(e) {
|
||||||
|
// let tarEle = e.target
|
||||||
|
|
||||||
|
// if (tarEle.id === "hourInput") {
|
||||||
|
// if (parseInt(tarEle.value) > 12) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (tarEle.id === "minuteInput") {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// if (tarEle.id === "amPmInput") {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
196
application/static/styles.css
Normal file
196
application/static/styles.css
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#radios {
|
||||||
|
position: relative;
|
||||||
|
background-color:rgba(239,239,240,1);
|
||||||
|
z-index:5;
|
||||||
|
width: 245.5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bckgrnd,
|
||||||
|
.labels {
|
||||||
|
width: 120px;
|
||||||
|
height: 66px;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: -3px;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels {
|
||||||
|
padding-top: 7.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bckgrnd {
|
||||||
|
width: 115px;
|
||||||
|
height: 55px;
|
||||||
|
background-color: white;
|
||||||
|
border: .5px solid rgba(0,0,0,0.04);
|
||||||
|
box-shadow: 0 3px 8px 0 rgba(0,0,0,0.12), 0 3px 1px 0 rgba(0,0,0,0.04);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
border-radius: 7px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rad1:checked ~ #bckgrnd {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rad2:checked ~ #bckgrnd {
|
||||||
|
transform: translateX(120px);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 1rem auto 0;
|
||||||
|
outline: 1px solid #ccc;
|
||||||
|
padding: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-container {
|
||||||
|
width: 80px;
|
||||||
|
height: 210px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.25;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
cursor: default;
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-prev,
|
||||||
|
.swiper-slide-next {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vizor {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
height: 70px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vizor:before,
|
||||||
|
.vizor:after {
|
||||||
|
content: ':';
|
||||||
|
display: inline-block;
|
||||||
|
line-height: inherit;
|
||||||
|
height: 100%;
|
||||||
|
position:absolute;
|
||||||
|
top: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vizor:before {
|
||||||
|
left: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vizor:after {
|
||||||
|
left: 175px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrows .swiper-container:after,
|
||||||
|
.arrows .swiper-container:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 4px;
|
||||||
|
border-color: transparent;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrows .swiper-container:before {
|
||||||
|
top: 0.5rem;
|
||||||
|
border-top-width: 0;
|
||||||
|
border-bottom-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrows .swiper-container:after {
|
||||||
|
bottom: 0.5rem;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
border-top-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*.donate-now {*/
|
||||||
|
/* list-style-type: none;*/
|
||||||
|
/* margin: 25px 0 0 0;*/
|
||||||
|
/* padding: 0;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
/*.donate-now li {
|
||||||
|
float: left;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*.donate-now label,
|
||||||
|
.donate-now input {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*.donate-now input[type="radio"] {
|
||||||
|
opacity: 0.01;
|
||||||
|
z-index: 100;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.donate-now input[type="radio"]:checked+label { /*, .Checked+label */
|
||||||
|
border-color: rgb(79 70 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.donate-now label {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donate-now label:hover {
|
||||||
|
background: #DDD;
|
||||||
|
}
|
BIN
application/sudoku.png
Normal file
BIN
application/sudoku.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
190
application/sudoku_generator.py
Normal file
190
application/sudoku_generator.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
from PIL import Image
|
||||||
|
from PIL import ImageDraw
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
def generate_sudoku():
|
||||||
|
base = 3
|
||||||
|
side = base*base
|
||||||
|
|
||||||
|
# pattern for a baseline valid solution
|
||||||
|
def pattern(r,c): return (base*(r%base)+r//base+c)%side
|
||||||
|
|
||||||
|
# randomize rows, columns and numbers (of valid base pattern)
|
||||||
|
from random import sample
|
||||||
|
def shuffle(s): return sample(s,len(s))
|
||||||
|
rBase = range(base)
|
||||||
|
rows = [ g*base + r for g in shuffle(rBase) for r in shuffle(rBase) ]
|
||||||
|
cols = [ g*base + c for g in shuffle(rBase) for c in shuffle(rBase) ]
|
||||||
|
nums = shuffle(range(1,base*base+1))
|
||||||
|
|
||||||
|
# produce board using randomized baseline pattern
|
||||||
|
board = [ [nums[pattern(r,c)] for c in cols] for r in rows ]
|
||||||
|
|
||||||
|
# for line in board: print(line)
|
||||||
|
# solution = board
|
||||||
|
# print(solution)
|
||||||
|
|
||||||
|
|
||||||
|
squares = side*side
|
||||||
|
empties = squares * 3//5
|
||||||
|
# empties = squares * diff
|
||||||
|
# print(squares)
|
||||||
|
# print(empties)
|
||||||
|
# print(squares * 13//20)
|
||||||
|
for p in sample(range(squares),empties):
|
||||||
|
board[p//side][p%side] = 0
|
||||||
|
|
||||||
|
# numSize = len(str(side))
|
||||||
|
# for line in board:
|
||||||
|
# print(*(f"{n or '.':{numSize}} " for n in line))
|
||||||
|
|
||||||
|
|
||||||
|
def expandLine(line):
|
||||||
|
# return line[0]+line[5:9].join([line[1:5]*(base-1)]*base)+line[9:13]
|
||||||
|
# return line[0]+line[3:7].join([line[1:3]*(base-1)]*base)+line[3:5]
|
||||||
|
# return line[0]+line[3:5].join([line[1:3]*(base-1)]*base)+line[5:7]
|
||||||
|
return line[0]+line[4:7].join([line[1:4]*(base-1)]*base)+line[7:10]
|
||||||
|
# line0 = expandLine("╔═══╤═══╦═══╗")
|
||||||
|
# line1 = expandLine("║ . │ . ║ . ║")
|
||||||
|
# line2 = expandLine("╟───┼───╫───╢")
|
||||||
|
# line3 = expandLine("╠═══╪═══╬═══╣")
|
||||||
|
# line4 = expandLine("╚═══╧═══╩═══╝")
|
||||||
|
# line0 = expandLine("╔═╤═╦═╗")
|
||||||
|
# line1 = expandLine("║.│.║.║")
|
||||||
|
# line2 = expandLine("╟─┼─╫─╢")
|
||||||
|
# line3 = expandLine("╠═╪═╬═╣")
|
||||||
|
# line4 = expandLine("╚═╧═╩═╝")
|
||||||
|
# line0 = expandLine("╔═╤═╦═╗")
|
||||||
|
# line1 = expandLine("║.│.║.║")
|
||||||
|
# line2 = expandLine("╟─┼─╫─╢")
|
||||||
|
# line3 = expandLine("╠═╪═╬═╣")
|
||||||
|
# line4 = expandLine("╚═╧═╩═╝")
|
||||||
|
line0 = expandLine("╔══╤══╦══╗")
|
||||||
|
line1 = expandLine("║. │. ║. ║")
|
||||||
|
line2 = expandLine("╟──┼──╫──╢")
|
||||||
|
line3 = expandLine("╠══╪══╬══╣")
|
||||||
|
line4 = expandLine("╚══╧══╩══╝")
|
||||||
|
|
||||||
|
symbol = " 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
nums = [ [""]+[symbol[n] for n in row] for row in board ]
|
||||||
|
output = []
|
||||||
|
# print(line0)
|
||||||
|
output.append(line0)
|
||||||
|
for r in range(1,side+1):
|
||||||
|
output.append("".join(n+s for n,s in zip(nums[r-1],line1.split("."))))
|
||||||
|
output.append([line2,line3,line4][(r%side==0)+(r%base==0)])
|
||||||
|
# print( "".join(n+s for n,s in zip(nums[r-1],line1.split("."))) )
|
||||||
|
# print([line2,line3,line4][(r%side==0)+(r%base==0)])
|
||||||
|
|
||||||
|
# print('=')
|
||||||
|
# print(output)
|
||||||
|
# for x in output:
|
||||||
|
# print(x)
|
||||||
|
# print('-')
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_image(sudoku):
|
||||||
|
# sample text and font
|
||||||
|
# unicode_text = u"Unicode Characters: \u00C6 \u00E6 \u00B2 \u00C4 \u00D1 \u220F"
|
||||||
|
unicode_text = sudoku[0]
|
||||||
|
verdana_font = ImageFont.truetype("IBMPlexMono-Medium.ttf", 16, encoding="unic")
|
||||||
|
|
||||||
|
# get the line size
|
||||||
|
text_width, text_height = verdana_font.getsize(unicode_text)
|
||||||
|
|
||||||
|
# create a blank canvas with extra space between lines
|
||||||
|
canvas = Image.new('RGB', (text_width + 10, (text_height*(len(sudoku)-1))), (255, 255, 255))
|
||||||
|
# canvas.convert('L')
|
||||||
|
|
||||||
|
|
||||||
|
# draw the text onto the text canvas, and use black as the text color
|
||||||
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
|
# for x in sudoku:
|
||||||
|
pos = 0
|
||||||
|
for x in sudoku:
|
||||||
|
unicode_text = x
|
||||||
|
draw.text((5,pos), unicode_text, font = verdana_font, fill = "#000000")
|
||||||
|
pos += (text_height-2)
|
||||||
|
|
||||||
|
# save the blank canvas to a file
|
||||||
|
# fn = lambda x : 255 if x > 200 else 0
|
||||||
|
# canvas.convert('L').point(fn, mode='1')
|
||||||
|
canvas.save("sudoku.png", "PNG")
|
||||||
|
# img = Image.new('RGB', (200, 100))
|
||||||
|
# d = ImageDraw.Draw(img)
|
||||||
|
# d.text((30, 20), sudoku[0], fill=(255, 0, 0))
|
||||||
|
# text_width, text_height = d.textsize(sudoku[0])
|
||||||
|
# print(text_width, text_height)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
su = generate_sudoku()
|
||||||
|
for x in su:
|
||||||
|
print(x)
|
||||||
|
convert_to_image(su)
|
||||||
|
print(su)
|
||||||
|
# for x in generate_sudoku():
|
||||||
|
# print(x)
|
||||||
|
|
||||||
|
|
||||||
|
# print(board)
|
||||||
|
|
||||||
|
# import random
|
||||||
|
# from itertools import islice
|
||||||
|
# print([*islice(shortSudokuSolve(board),2)][0])
|
||||||
|
# print([*islice(shortSudokuSolve(board),2)][1])
|
||||||
|
# if [*islice(shortSudokuSolve(board),2)][0] == [*islice(shortSudokuSolve(board),2)][1]:
|
||||||
|
# print('ay')
|
||||||
|
# else:
|
||||||
|
# print('er')
|
||||||
|
|
||||||
|
|
||||||
|
# while True:
|
||||||
|
# solved = [*islice(shortSudokuSolve(board),2)]
|
||||||
|
# # print(len(solved))
|
||||||
|
# # print(solved)
|
||||||
|
# if len(solved)==1:
|
||||||
|
# # print('yay!')
|
||||||
|
# break
|
||||||
|
# diffPos = [(r,c) for r in range(9) for c in range(9)
|
||||||
|
# if solved[0][r][c] != solved[1][r][c] ]
|
||||||
|
# r,c = random.choice(diffPos)
|
||||||
|
# board[r][c] = solution[r][c]
|
||||||
|
# # print(board)
|
||||||
|
# print(r,c)
|
||||||
|
# # print(board)
|
||||||
|
|
||||||
|
|
||||||
|
# print(board)
|
||||||
|
# print(solution)
|
||||||
|
# print('ysy')
|
||||||
|
|
||||||
|
# def expandLine(line):
|
||||||
|
# return line[0]+line[5:9].join([line[1:5]*(base-1)]*base)+line[9:13]
|
||||||
|
# line0 = expandLine("╔═══╤═══╦═══╗")
|
||||||
|
# line1 = expandLine("║ . │ . ║ . ║")
|
||||||
|
# line2 = expandLine("╟───┼───╫───╢")
|
||||||
|
# line3 = expandLine("╠═══╪═══╬═══╣")
|
||||||
|
# line4 = expandLine("╚═══╧═══╩═══╝")
|
||||||
|
|
||||||
|
# symbol = " 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
# nums = [ [""]+[symbol[n] for n in row] for row in board ]
|
||||||
|
# output = []
|
||||||
|
# print(line0)
|
||||||
|
# output.append(line0)
|
||||||
|
# for r in range(1,side+1):
|
||||||
|
# output.append("".join(n+s for n,s in zip(nums[r-1],line1.split("."))))
|
||||||
|
# output.append([line2,line3,line4][(r%side==0)+(r%base==0)])
|
||||||
|
# print( "".join(n+s for n,s in zip(nums[r-1],line1.split("."))) )
|
||||||
|
# print([line2,line3,line4][(r%side==0)+(r%base==0)])
|
||||||
|
|
||||||
|
# print('==')
|
||||||
|
# print(output)
|
||||||
|
# for x in output:
|
||||||
|
# print(x)
|
||||||
|
# print('--')
|
||||||
|
|
||||||
|
# return output
|
||||||
|
|
7
application/sync_calendar.py
Normal file
7
application/sync_calendar.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#TODO: All Google/iCloud Calendar syncing logic here
|
||||||
|
|
||||||
|
class SyncData():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
#TODO: Obtain calendar data and save to local database, parse into printable content
|
178
application/thermal_print.py
Normal file
178
application/thermal_print.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# from PyESCPOS.impl.epson import GenericESCPOS
|
||||||
|
# from PyESCPOS.ifusb import USBConnection
|
||||||
|
|
||||||
|
# conn = USBConnection.create('5011:0416,interface=0,ep_out=3,ep_in=0')
|
||||||
|
# printer = ElginRM22(conn)
|
||||||
|
# printer.init()
|
||||||
|
# printer.text('Hello World!')
|
||||||
|
|
||||||
|
|
||||||
|
# p = Usb(0x5011, 0x0416, printer="simple")
|
||||||
|
# p.text("Hello World\n")
|
||||||
|
|
||||||
|
# p = printer.Usb(0x0416, 0x5011, 4, 0x81, 0x02) #initalize printer on raspberry pi
|
||||||
|
# p.text("hello world") #print this text
|
||||||
|
# p.cut() #move paper up enough to tear off (probably a better way, but this works.)
|
||||||
|
|
||||||
|
# import falcon
|
||||||
|
# from wsgiref import simple_server
|
||||||
|
# import os
|
||||||
|
|
||||||
|
|
||||||
|
# import os
|
||||||
|
# import falcon
|
||||||
|
|
||||||
|
# WORKING_DIRECTORY = os.getcwd()
|
||||||
|
# STATIC = 'static/'
|
||||||
|
|
||||||
|
# def apex(req, resp):
|
||||||
|
# resp.content_type = 'text/html; charset=utf-8'
|
||||||
|
# filename = os.path.abspath(os.path.join(WORKING_DIRECTORY, STATIC, 'index.html'))
|
||||||
|
# with open(filename, 'rt') as f:
|
||||||
|
# resp.body = f.read()
|
||||||
|
from escpos.printer import Usb
|
||||||
|
import usb
|
||||||
|
import wonderwords
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
from sudoku_generator import *
|
||||||
|
from wordsearch_generator import WordSearch
|
||||||
|
from database import TodoDatabase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ThermalPrinter():
|
||||||
|
def __init__(self, database):
|
||||||
|
# self.p = printer.Usb(0x0416, 0x5011, 4, 0x81, 0x02) #initalize printer on raspberry pi
|
||||||
|
self.database = database
|
||||||
|
try:
|
||||||
|
self.p = Usb(0x0416, 0x5011, 4, 0x81, 0x02) #initalize printer on raspberry pi
|
||||||
|
# pass
|
||||||
|
except usb.core.NoBackendError as e:
|
||||||
|
print(e)
|
||||||
|
print("Try running `export DYLD_LIBRARY_PATH=/opt/homebrew/lib` if on m1 mac. source: https://github.com/pyusb/pyusb/issues/355#issuecomment-1062798576")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def print_greeting(self):
|
||||||
|
self.p.set(align="center")
|
||||||
|
self.p.set(invert=True)
|
||||||
|
self.p.text(datetime.today().strftime('%Y-%m-%d'))
|
||||||
|
self.p.set(align="left")
|
||||||
|
self.p.set(invert=False)
|
||||||
|
# pass #TODO: add other greetings?
|
||||||
|
|
||||||
|
def print_todos(self, todos=[{"time": "1:00pm to 2:00pm", "text": "Read a book"}]):
|
||||||
|
for x in self._parse_todos(todos):
|
||||||
|
# print(x)
|
||||||
|
self.p.set(align="left")
|
||||||
|
self.p.text(x[0])
|
||||||
|
self.p.set(align="right")
|
||||||
|
self.p.textln(x[1])
|
||||||
|
self.p.set(align="left")
|
||||||
|
|
||||||
|
def print_sudoku(self):
|
||||||
|
self.p.close()
|
||||||
|
self.p = Usb(0x0416, 0x5011, 4, 0x81, 0x02)
|
||||||
|
convert_to_image(generate_sudoku())
|
||||||
|
self.p.image("sudoku.png")
|
||||||
|
|
||||||
|
def print_random_quote(self):
|
||||||
|
q = self.database.get_random_quote()
|
||||||
|
self.p.set(align="left")
|
||||||
|
self.p.text(q[0])
|
||||||
|
self.p.set(align="right")
|
||||||
|
self.p.text(q[1])
|
||||||
|
|
||||||
|
# pass #TODO: parse https://zenquotes.io/api/quotes (api limit is 5 req per 30 seconds)
|
||||||
|
|
||||||
|
def print_wordsearch(self):
|
||||||
|
# words = ("gugu,gaga")
|
||||||
|
words = r.random_words(10, include_parts_of_speech=["nouns", "verbs", "adjectives"])
|
||||||
|
w = WordSearch(words, 32, 32)
|
||||||
|
|
||||||
|
for x in w.grid:
|
||||||
|
self.p.set(align="center")
|
||||||
|
self.p.text(x + "\n")
|
||||||
|
|
||||||
|
# Defaults.NOUNS: Represents a list of nouns
|
||||||
|
# Defaults.VERBS: Represents a list of verbs
|
||||||
|
# Defaults.ADJECTIVES: Represents a list of adjectives
|
||||||
|
|
||||||
|
|
||||||
|
# def printGrid(grid):
|
||||||
|
# for row in grid:
|
||||||
|
# for column in row:
|
||||||
|
# print("%s" % column, end='')
|
||||||
|
# print()
|
||||||
|
# printGrid(w.grid)
|
||||||
|
# w.findWords(words.split(','))
|
||||||
|
# print(w.wordPosition)
|
||||||
|
|
||||||
|
def print_default(self):
|
||||||
|
self.print_greeting()
|
||||||
|
self.print_todos()
|
||||||
|
self.print_random_quote()
|
||||||
|
self.print_sudoku()
|
||||||
|
self.print_wordsearch()
|
||||||
|
self.finished_printing()
|
||||||
|
|
||||||
|
def _parse_todos(self, data):
|
||||||
|
out = []
|
||||||
|
for x in data:
|
||||||
|
out.append(["○ " + x["time"], x["text"]])
|
||||||
|
return out
|
||||||
|
|
||||||
|
#Return: ["□ 1:00pm to 2:00pm", "Read a book"]
|
||||||
|
|
||||||
|
def finished_printing(self):
|
||||||
|
self.p.cut() #move paper up enough to tear off (probably a better way, but this works.)
|
||||||
|
|
||||||
|
|
||||||
|
# class SyncData():
|
||||||
|
# def __init__(self):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# #TODO: Obtain calendar data and save to local database, parse into printable content
|
||||||
|
|
||||||
|
|
||||||
|
# class GetTodos:
|
||||||
|
# def on_post(self, req, resp):
|
||||||
|
# pass #Handle web interface requesting current todos
|
||||||
|
|
||||||
|
# class SaveTodos:
|
||||||
|
# def on_post(self, req, resp):
|
||||||
|
# pass #Handle web interface sending updated todo list
|
||||||
|
|
||||||
|
# class PrintTodos:
|
||||||
|
# def on_post(self, req, resp):
|
||||||
|
# pass #Handle web interface requesting a print action
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = TodoDatabase()
|
||||||
|
|
||||||
|
tp = ThermalPrinter(data)
|
||||||
|
# tp.print_todos()
|
||||||
|
tp.print_default()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
# def main():
|
||||||
|
# app = falcon.App()
|
||||||
|
|
||||||
|
# # app.add_route('/stories', things)
|
||||||
|
# app.add_sink(apex, prefix='^/$')
|
||||||
|
# app.add_static_route('/', os.path.abspath(os.path.join(WORKING_DIRECTORY, STATIC)))
|
||||||
|
|
||||||
|
# # print(os.path.abspath(''))
|
||||||
|
# # app.add_static_route('/', os.path.abspath(''))
|
||||||
|
|
||||||
|
# httpd = simple_server.make_server('127.0.0.1', 8000, app)
|
||||||
|
# httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
# if __name__ == '__main__':
|
||||||
|
# main()
|
BIN
application/unicode-text.png
Normal file
BIN
application/unicode-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
78
application/utils.py
Normal file
78
application/utils.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import SecurityScopes, HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
|
from application.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(HTTPException):
|
||||||
|
def __init__(self, detail: str, **kwargs):
|
||||||
|
"""Returns HTTP 403"""
|
||||||
|
super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthenticatedException(HTTPException):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyToken:
|
||||||
|
"""Does all the token verification using PyJWT"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = get_settings()
|
||||||
|
|
||||||
|
# This gets the JWKS from a given URL and does processing so you can
|
||||||
|
# use any of the keys available
|
||||||
|
jwks_url = f'https://{self.config.auth0_domain}/.well-known/jwks.json'
|
||||||
|
self.jwks_client = jwt.PyJWKClient(jwks_url)
|
||||||
|
|
||||||
|
async def verify(self,
|
||||||
|
security_scopes: SecurityScopes,
|
||||||
|
token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer())
|
||||||
|
):
|
||||||
|
if token is None:
|
||||||
|
raise UnauthenticatedException
|
||||||
|
|
||||||
|
# This gets the 'kid' from the passed token
|
||||||
|
try:
|
||||||
|
signing_key = self.jwks_client.get_signing_key_from_jwt(
|
||||||
|
token.credentials
|
||||||
|
).key
|
||||||
|
except jwt.exceptions.PyJWKClientError as error:
|
||||||
|
raise UnauthorizedException(str(error))
|
||||||
|
except jwt.exceptions.DecodeError as error:
|
||||||
|
raise UnauthorizedException(str(error))
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token.credentials,
|
||||||
|
signing_key,
|
||||||
|
algorithms=self.config.auth0_algorithms,
|
||||||
|
audience=self.config.auth0_api_audience,
|
||||||
|
issuer=self.config.auth0_issuer,
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
raise UnauthorizedException(str(error))
|
||||||
|
|
||||||
|
if len(security_scopes.scopes) > 0:
|
||||||
|
self._check_claims(payload, 'scope', security_scopes.scopes)
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _check_claims(self, payload, claim_name, expected_value):
|
||||||
|
if claim_name not in payload:
|
||||||
|
raise UnauthorizedException(detail=f'No claim "{claim_name}" found in token')
|
||||||
|
|
||||||
|
payload_claim = payload[claim_name]
|
||||||
|
|
||||||
|
if claim_name == 'scope':
|
||||||
|
payload_claim = payload[claim_name].split(' ')
|
||||||
|
|
||||||
|
for value in expected_value:
|
||||||
|
if value not in payload_claim:
|
||||||
|
raise UnauthorizedException(detail=f'Missing "{claim_name}" scope')
|
213
application/wordsearch_generator.py
Normal file
213
application/wordsearch_generator.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
#https://github.com/mast3rsoft/WordSearch/blob/master/WordSearch.py
|
||||||
|
import random
|
||||||
|
|
||||||
|
class WordSearch():
|
||||||
|
HORIZONTAL = 0
|
||||||
|
VERTICAL = 1
|
||||||
|
DIAGONAL = 2
|
||||||
|
REVHORIZONTAL = 3
|
||||||
|
REVVERTICAL = 4
|
||||||
|
REVDIAGONAL = 5
|
||||||
|
REVFLIPDIAGONA = 6
|
||||||
|
FLIPDIAGONAL = 7
|
||||||
|
DONTCARE = -100
|
||||||
|
wordPosition = {}
|
||||||
|
def __init__(self, searchWords, maxX = 20, maxY = 20):
|
||||||
|
self.maxX = maxX
|
||||||
|
self.maxY = maxY
|
||||||
|
self.grid = [] # grid is a list of list of strings (characters)
|
||||||
|
testWords = ['superhero', 'gugu','gaga','blah','vodka']
|
||||||
|
searchWords = searchWords.split(",")
|
||||||
|
if searchWords == ['']:
|
||||||
|
searchWords = testWords
|
||||||
|
self.searchWords = searchWords
|
||||||
|
for row in range(0, self.maxY):
|
||||||
|
self.grid.append([])
|
||||||
|
for column in range(0, self.maxX):
|
||||||
|
self.grid[row].append('*')
|
||||||
|
for word in searchWords:
|
||||||
|
DIR = random.randint(0, 7)
|
||||||
|
while not self.engrave(word, self.DONTCARE , self.DONTCARE , DIR):
|
||||||
|
pass
|
||||||
|
self.obfusticate()
|
||||||
|
def engrave(self, word, x, y, direction):
|
||||||
|
if len(word) == 0:
|
||||||
|
return True
|
||||||
|
# word has length > 0
|
||||||
|
# check if we need to choose random pos
|
||||||
|
if x == self.DONTCARE or y == self.DONTCARE: # cannot have one random, one fixed
|
||||||
|
while True:
|
||||||
|
y = random.randint(0, self.maxY - 1)
|
||||||
|
x = random.randint(0, self.maxX - 1)
|
||||||
|
if self.grid[y][x] == '*':
|
||||||
|
break
|
||||||
|
# check if x & y are valid
|
||||||
|
if x == self.maxX or x < 0:
|
||||||
|
return False
|
||||||
|
if y == self.maxY or y < 0:
|
||||||
|
return False
|
||||||
|
if not (self.grid[y][x] == "*" or self.grid[y][x] == word[0]):
|
||||||
|
return False
|
||||||
|
undovalue = self.grid[y][x]
|
||||||
|
undox = x
|
||||||
|
undoy = y
|
||||||
|
self.grid[y][x] = word[0]
|
||||||
|
# now need to write rest of the word
|
||||||
|
if direction == self.HORIZONTAL:
|
||||||
|
x += 1
|
||||||
|
elif direction == self.VERTICAL:
|
||||||
|
y += 1
|
||||||
|
elif direction == self.DIAGONAL:
|
||||||
|
y += 1
|
||||||
|
x += 1
|
||||||
|
elif direction == self.REVHORIZONTAL:
|
||||||
|
x -= 1
|
||||||
|
elif direction == self.REVVERTICAL:
|
||||||
|
y -= 1
|
||||||
|
elif direction == self.REVDIAGONAL:
|
||||||
|
y -= 1
|
||||||
|
x -= 1
|
||||||
|
elif direction == self.FLIPDIAGONAL:
|
||||||
|
x += 1
|
||||||
|
y -= 1
|
||||||
|
elif direction == self.REVFLIPDIAGONA:
|
||||||
|
x -= 1
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
print("This direction not implemented yet")
|
||||||
|
if self.engrave(word[1:], x, y, direction):
|
||||||
|
# we could do the rest, we are happy and done
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# grrh: something didn’t work, we need to undo now
|
||||||
|
y = undoy
|
||||||
|
x = undox
|
||||||
|
self.grid[y][x] = undovalue
|
||||||
|
return False
|
||||||
|
def obfusticate(self):
|
||||||
|
for row in self.grid:
|
||||||
|
for i in range(len(row)):
|
||||||
|
if row[i] == '*':
|
||||||
|
row[i] = 'abcdefghijklmnopqrstuvwxyz0123456789'[random.randint(0,25)]
|
||||||
|
def letter(self,x,y):
|
||||||
|
return self.grid[x][y]
|
||||||
|
def findWords(self, words):
|
||||||
|
for word in words:
|
||||||
|
firstLetter = word[0]
|
||||||
|
positions = None
|
||||||
|
y = 0; found = False
|
||||||
|
while y < self.maxY and not found:
|
||||||
|
x = 0
|
||||||
|
while x < self.maxX and not found:
|
||||||
|
if firstLetter == self.grid[y][x]:
|
||||||
|
positions = self.wordIsHere(word, x, y)
|
||||||
|
if positions:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
x += 1
|
||||||
|
if not found:
|
||||||
|
y += 1
|
||||||
|
if found:
|
||||||
|
self.wordPosition[word] = positions
|
||||||
|
def wordIsHere(self,word, firstX, firstY):
|
||||||
|
maxX = self.maxX
|
||||||
|
maxY = self.maxY
|
||||||
|
# horizontal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if x == maxX or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x += 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# vertical
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == maxY or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
y += 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# reverse horizontal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if x == -1 or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x -= 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# reverse vertical
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == -1 or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
y -= 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# diagonal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == maxY or x == maxX or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x += 1
|
||||||
|
y += 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# reverse diagonal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == -1 or x == -1 or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x -= 1
|
||||||
|
y -= 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# flip diagonal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == -1 or x == maxX or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x += 1
|
||||||
|
y -= 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
# reverse flip diagonal
|
||||||
|
found = True; x = firstX; y = firstY; positions = []
|
||||||
|
for letter in word:
|
||||||
|
if y == maxY or x == -1 or letter != self.grid[y][x]:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
positions.append((y, x))
|
||||||
|
x -= 1
|
||||||
|
y += 1
|
||||||
|
if found:
|
||||||
|
return positions
|
||||||
|
#
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Test Progamm for WordSearch Genertator
|
||||||
|
if __name__ == '__main__':
|
||||||
|
words = ("gugu,gaga")
|
||||||
|
w = WordSearch(words, 10, 5)
|
||||||
|
def printGrid(grid):
|
||||||
|
for row in grid:
|
||||||
|
for column in row:
|
||||||
|
print("%s" % column, end='')
|
||||||
|
print()
|
||||||
|
printGrid(w.grid)
|
||||||
|
w.findWords(words.split(','))
|
||||||
|
print(w.wordPosition)
|
1
data.json
Normal file
1
data.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"2023-02-23": [{"time": "1:00pm to 2:00pm", "task": "Read a book."}]}
|
20
requirements.txt
Normal file
20
requirements.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
falcon
|
||||||
|
python-escpos
|
||||||
|
annotated-types==0.5.0
|
||||||
|
anyio==3.7.1
|
||||||
|
asgiref==3.7.2
|
||||||
|
cffi==1.15.1
|
||||||
|
click==8.1.6
|
||||||
|
cryptography==40.0.2
|
||||||
|
fastapi==0.100.1
|
||||||
|
h11==0.14.0
|
||||||
|
idna==3.4
|
||||||
|
pycparser==2.21
|
||||||
|
pydantic==2.1.1
|
||||||
|
pydantic-settings==2.0.2
|
||||||
|
pydantic_core==2.4.0
|
||||||
|
PyJWT==2.8.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
sniffio==1.3.0
|
||||||
|
typing_extensions==4.7.1
|
||||||
|
uvicorn==0.23.2
|
Loading…
Reference in New Issue
Block a user