add tailwindcss

This commit is contained in:
Waylon S. Walker 2024-10-14 19:56:41 -05:00
parent a426df12c0
commit 16e207000f
20 changed files with 2421 additions and 100 deletions

View file

@ -1,4 +1,53 @@
default:
@just --choose
venv:
uv venv
run:
uv run -- uvicorn --reload --log-level debug src.fastapi_dynamic_response.main:app
get:
http GET :8000/example
get-plain:
http GET :8000/exa Content-Type=text/plain
get-rtf:
http GET :8000/example Content-Type=application/rtf
get-json:
http GET :8000 Content-Type=application/json
get-html:
http GET :8000 Content-Type=text/html
get-md:
http GET :8000 Content-Type=application/markdown
livez:
http GET :8000/livez
healthz:
http GET :8000/healthz
readyz:
http GET :8000/readyz
# Install Tailwind CSS
install-tailwind:
npm install tailwindcss
# Run Tailwind CLI to generate the CSS
build-tailwind:
npx tailwindcss -i ./tailwind/input.css -o ./static/app.css --minify
# Watch for changes and rebuild CSS automatically
watch-tailwind:
npx tailwindcss -i ./tailwind/input.css -o ./static/app.css --watch
# Remove node_modules (cleanup)
clean-node_modules:
rm -rf node_modules
# Install dependencies and build CSS
setup-tailwind: install-tailwind build-tailwind

1386
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"tailwindcss": "^3.4.13"
}
}

View file

@ -71,3 +71,35 @@ exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
[tool.ruff.lint]
select = [
"A",
"ARG",
"B",
"C",
"DTZ",
"E",
"EM",
"F",
"FBT",
"I",
"ICN",
# "ISC",
"N",
"PLC",
"PLE",
"PLR",
"PLW",
"Q",
"RUF",
"S",
"T",
"TID",
"UP",
"W",
"YTT",
]
[tool.ruff.lint.isort]
force-single-line = true

View file

@ -1,8 +1,10 @@
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter
from fastapi import Depends
from fastapi import Request
from fastapi_dynamic_response.base.schema import Message
from fastapi_dynamic_response.dependencies import get_content_type
router = APIRouter()

View file

@ -0,0 +1,21 @@
ACCEPT_TYPES = {
"application/json": "JSON",
"text/html": "html",
"application/html": "html",
"text/html-partial": "html",
"text/html-fragment": "html",
"text/rich": "rtf",
"application/rtf": "rtf",
"text/rtf": "rtf",
"text/plain": "text",
"application/text": "text",
"application/markdown": "markdown",
"text/markdown": "markdown",
"text/x-markdown": "markdown",
"json": "JSON",
"html": "html",
"rtf": "rtf",
"plain": "text",
"markdown": "markdown",
"md": "markdown",
}

View file

@ -2,3 +2,4 @@ from fastapi.templating import Jinja2Templates
is_ready = False
templates = Jinja2Templates(directory="templates")
routes = []

View file

@ -1,19 +1,37 @@
from fastapi import Depends, FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi_dynamic_response import globals
from fastapi_dynamic_response.__about__ import __version__
from fastapi_dynamic_response.base.router import router as base_router
from fastapi_dynamic_response.dependencies import get_content_type
from fastapi_dynamic_response.middleware import (
Sitemap,
catch_exceptions_middleware,
respond_based_on_content_type,
set_prefers,
)
from fastapi_dynamic_response.zpages.router import router as zpages_router
app = FastAPI(debug=True)
app = FastAPI(
title="FastAPI Dynamic Response",
version=__version__,
docs_url=None,
redoc_url=None,
openapi_url=None,
# openapi_tags=tags_metadata,
# exception_handlers=exception_handlers,
debug=True,
dependencies=[
Depends(set_prefers),
],
)
app.include_router(zpages_router)
app.include_router(base_router)
app.middleware("http")(Sitemap(app))
app.middleware("http")(catch_exceptions_middleware)
app.middleware("http")(respond_based_on_content_type)
app.mount("/static", StaticFiles(directory="static"), name="static")
# Flag to indicate if the application is ready
@ -24,6 +42,7 @@ async def startup_event():
# Perform startup actions, e.g., database connections
# If all startup actions are successful, set is_ready to True
globals.is_ready = True
globals.routes = [route.path for route in app.router.routes if route.path]
@app.get("/sitemap")

View file

@ -1,4 +1,9 @@
from difflib import get_close_matches
from io import BytesIO
import json
import traceback
from typing import Any, Dict
from fastapi import Request, Response
from fastapi.exceptions import (
HTTPException as StarletteHTTPException,
@ -6,19 +11,87 @@ from fastapi.exceptions import (
)
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
import html2text
from io import BytesIO
import json
from pydantic import BaseModel, model_validator
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import traceback
from weasyprint import HTML as WeasyHTML
from weasyprint import HTML as WEAZYHTML
from fastapi_dynamic_response.constant import ACCEPT_TYPES
from fastapi_dynamic_response.globals import templates
class Prefers(BaseModel):
JSON: bool = False
html: bool = False
rtf: bool = False
text: bool = False
markdown: bool = False
partial: bool = False
@property
def textlike(self) -> bool:
return self.rtf or self.text or self.markdown
@model_validator(mode="after")
def check_one_true(self) -> Dict[str, Any]:
format_flags = [self.JSON, self.html, self.rtf, self.text, self.markdown]
if format_flags.count(True) != 1:
message = "Exactly one of JSON, html, rtf, text, or markdown must be True."
raise ValueError(message)
def set_prefers(
request: Request,
):
content_type = (
request.query_params.get("content_type")
or request.headers.get("content-type")
or request.headers.get("accept", None)
).lower()
if content_type == "*/*":
content_type = None
hx_request_header = request.headers.get("hx-request")
user_agent = request.headers.get("user-agent", "").lower()
if hx_request_header == "true":
request.state.prefers = Prefers(html=True, partial=True)
return
if is_browser_request(user_agent) and content_type is None:
content_type = "text/html"
elif is_rtf_request(user_agent) and content_type is None:
content_type = "text/rtf"
elif content_type is None:
content_type = "application/json"
# if content_type in ACCEPT_TYPES:
for accept_type, accept_value in ACCEPT_TYPES.items():
if accept_type in content_type:
request.state.prefers = Prefers(**{ACCEPT_TYPES[accept_value]: True})
print("content_type:", content_type)
print("prefers:", request.state.prefers)
return
request.state.prefers = Prefers(JSON=True, partial=False)
print("prefers:", request.state.prefers)
print("content_type:", content_type)
class Sitemap:
def __init__(self, app):
self.app = app
async def __call__(self, request: Request, call_next):
request.state.routes = [
route.path for route in self.app.router.routes if route.path
]
return await call_next(request)
async def catch_exceptions_middleware(request: Request, call_next):
try:
return await call_next(request)
@ -102,17 +175,34 @@ def format_json_as_rich_text(data: dict, template_name: str) -> str:
return capture.get()
def handle_not_found(request: Request, data: str):
async def respond_based_on_content_type(
request: Request,
call_next,
content_type: str,
data: str,
):
requested_path = request.url.path
available_routes = [route.path for route in app.router.routes if route.path]
suggestions = get_close_matches(requested_path, available_routes, n=3, cutoff=0.5)
if requested_path in ["/docs", "/redoc", "/openapi.json"]:
return await call_next(request)
return await call_next(request)
def handle_not_found(request: Request, call_next, data: str):
requested_path = request.url.path
# available_routes = [route.path for route in app.router.routes if route.path]
suggestions = get_close_matches(
requested_path, request.state.routes, n=3, cutoff=0.5
)
request.state.template_name = "404.html"
return templates.TemplateResponse(
"404.html",
{
"request": request,
"data": json.loads(data),
"available_routes": available_routes,
"available_routes": request.state.routes,
"requested_path": requested_path,
"suggestions": suggestions,
},
@ -151,13 +241,17 @@ async def respond_based_on_content_type(request: Request, call_next):
data = body.decode("utf-8")
if response.status_code == 404:
return handle_not_found(request, data)
return handle_not_found(
request=request,
call_next=call_next,
data=data,
)
if response.status_code == 422:
return response
if str(response.status_code)[0] not in "123":
return response
return await handle_response(request, data, content_type)
return await handle_response(request, data)
# except TemplateNotFound:
# return HTMLResponse(content="Template Not Found ", status_code=404)
except StarletteHTTPException as exc:
@ -169,32 +263,26 @@ async def respond_based_on_content_type(request: Request, call_next):
return JSONResponse(status_code=422, content={"detail": exc.errors()})
except Exception as e:
print(traceback.format_exc())
return HTMLResponse(content=f"Internal Server Error: {str(e)}", status_code=500)
return HTMLResponse(content=f"Internal Server Error: {e!s}", status_code=500)
async def handle_response(request: Request, data: str, content_type: str):
async def handle_response(request: Request, data: str):
json_data = json.loads(data)
template_name = getattr(request.state, "template_name", "default_template.html")
if request.state.prefers.partial:
template_name = "partial_" + template_name
content_type = request.state.prefers
if content_type == "application/json":
if request.state.prefers.JSON:
return JSONResponse(content=json_data)
elif content_type == "text/html":
elif request.state.prefers.html:
return templates.TemplateResponse(
template_name, {"request": request, "data": json_data}
)
elif content_type == "text/html-partial":
return templates.TemplateResponse(
template_name, {"request": request, "data": json_data}
)
elif (
content_type == "text/markdown"
or content_type == "md"
or content_type == "markdown"
):
elif request.state.prefers.markdown:
import html2text
template = templates.get_template(template_name)
@ -202,15 +290,11 @@ async def handle_response(request: Request, data: str, content_type: str):
markdown_content = html2text.html2text(html_content)
return PlainTextResponse(content=markdown_content)
elif content_type == "text/plain":
elif request.state.prefers.text:
plain_text_content = format_json_as_plain_text(json_data)
return PlainTextResponse(content=plain_text_content)
elif (
content_type == "text/rich"
or content_type == "text/rtf"
or content_type == "application/rtf"
):
elif request.state.prefers.rtf:
rich_text_content = format_json_as_rich_text(json_data, template_name)
return PlainTextResponse(content=rich_text_content)
@ -223,7 +307,7 @@ async def handle_response(request: Request, data: str, content_type: str):
elif content_type == "application/pdf":
template = templates.get_template(template_name)
html_content = template.render(data=json_data)
pdf = WeasyHTML(string=html_content).write_pdf()
pdf = WEAZYHTML(string=html_content).write_pdf()
return Response(content=pdf, media_type="application/pdf")
return JSONResponse(content=json_data)

View file

@ -1,4 +1,5 @@
from fastapi import APIRouter, HTTPException, Request
from fastapi_dynamic_response import globals
router = APIRouter()
@ -36,7 +37,7 @@ async def healthz(request: Request):
Returns 503 Service Unavailable if not healthy.
"""
request.state.template_name = "status.html"
if is_ready:
if globals.is_ready:
return {"status": "healthy"}
else:
raise HTTPException(status_code=503, detail="Unhealthy")

674
static/app.css Normal file
View file

@ -0,0 +1,674 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mt-auto {
margin-top: auto;
}
.block {
display: block;
}
.flex {
display: flex;
}
.min-h-screen {
min-height: 100vh;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.p-4 {
padding: 1rem;
}
.text-center {
text-align: center;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.text-teal-400 {
--tw-text-opacity: 1;
color: rgb(45 212 191 / var(--tw-text-opacity));
}
.hover\:text-teal-300:hover {
--tw-text-opacity: 1;
color: rgb(94 234 212 / var(--tw-text-opacity));
}

9
tailwind.config.js Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./templates/**/*.html"],
theme: {
extend: {},
},
plugins: [],
}

3
tailwind/input.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,19 +1,23 @@
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>404 {{ requested_path }} not found</title>
</head>
<body>
<h2>Page Not Found</h2>
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block content %}
<h1>Page Not Found</h1>
<hr>
<h3>Suggestions</h3>
<h2>Suggestions</h2>
{% if suggestions %}
<p>
You're looking for {{ requested_path }}, but there's nothing here, here are some suggestions:
</p>
{% else %}
<p>
You're looking for {{ requested_path }}, but there's nothing here.
</p>
{% endif %}
{% for suggestion in suggestions %}
<li><a href="{{ suggestion }}">{{ suggestion }}</a> </li>
{% endfor %}
</body>
</html>
<p> Try checking our <a href='/sitemap'>sitemap</a></p>
{% endblock %}

View file

@ -1,10 +1,8 @@
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
{% extends "base.html" %}
{% block title %}Another Example{% endblock %}
{% block content %}
<h2>Example</h2>
<p>
{{ data.message }}
@ -19,6 +17,4 @@
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
{% endblock %}

26
templates/base.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}FastAPI Dynamic Response{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="/static/app.css" rel="stylesheet">
</head>
<body class="bg-gray-900 text-gray-200 min-h-screen flex flex-col">
<header class="bg-gray-800 p-4">
<div class="container mx-auto flex justify-between items-center">
<a href="/" class="text-xl font-bold text-teal-400">FastAPI Dynamic Response</a>
{% include "navigation.html" %}
</div>
</header>
<main class="container mx-auto p-4">
{% block content %}{% endblock %}
</main>
<footer class="bg-gray-800 text-center p-4 mt-auto justify-end">
<p>&copy; 2024 FastApi Dynamic Response</p>
</footer>
</body>
</html>

View file

@ -1,10 +1,8 @@
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
{% extends "base.html" %}
{% block title %}Example{% endblock %}
{% block content %}
<h2>Example</h2>
<p>
{{ data.message }}
@ -13,6 +11,4 @@
<h3>Data</h3>
{{ data.data }}
</body>
</html>
{% endblock %}

View file

@ -0,0 +1,6 @@
<nav class="space-x-4">
<a href="/example" class="hover:text-teal-300">Example</a>
<a href="/another-example" class="hover:text-teal-300">Another Example</a>
<a href="/message" class="hover:text-teal-300">Message</a>
<a href="/sitemap" class="hover:text-teal-300">Sitemap</a>
</nav>

View file

@ -1,13 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>siemap</title>
</head>
<body>
{% extends "base.html" %}
{% block title %}Sitemap{% endblock %}
{% block content %}
<h1>Sitemap</h1>
{% for route in data.available_routes %}
<li><a href="{{ route }}">{{ route }}</a> </li>
{% endfor %}
</body>
</html>
{% endblock %}

9
templates/status.html Normal file
View file

@ -0,0 +1,9 @@
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ data.status }}</title>
</head>
<body>
<h1>{{ data.status }}</h1>
</html>