First Working version. I can add table

This commit is contained in:
2025-05-10 16:55:52 +02:00
parent b708ef2c46
commit 2daff83e67
157 changed files with 17282 additions and 12 deletions

0
src/pages/__init__.py Normal file
View File

210
src/pages/admin.py Normal file
View File

@@ -0,0 +1,210 @@
from fasthtml.common import *
from core.user_dao import UserDAO
def admin_dashboard():
"""
Create the admin dashboard page.
Returns:
Components representing the admin dashboard
"""
return Div(
# Page header
H1("Admin Dashboard", cls="text-3xl font-bold text-gray-800 mb-6"),
# Admin menu
Div(
A(
Div(
Div(
"Users",
cls="text-xl font-semibold mb-2"
),
P("Manage user accounts, set admin privileges", cls="text-sm text-gray-600"),
cls="p-4"
),
href="/admin/users",
cls="block bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 mb-4"
),
A(
Div(
Div(
"Title Generation History",
cls="text-xl font-semibold mb-2"
),
P("View all users' title generation history", cls="text-sm text-gray-600"),
cls="p-4"
),
href="/admin/history",
cls="block bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 mb-4"
),
cls="max-w-2xl mx-auto"
)
)
def admin_users_page(page=1, error_message=None, success_message=None):
"""
Create the admin users management page.
Args:
page: Current page number
error_message: Optional error message
success_message: Optional success message
Returns:
Components representing the admin users page
"""
# Get users with pagination
limit = 10
offset = (page - 1) * limit
users = UserDAO.get_all_users(limit=limit, offset=offset)
# Create message alert if needed
message_alert = None
if error_message:
message_alert = Div(
P(error_message, cls="text-sm"),
cls="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"
)
elif success_message:
message_alert = Div(
P(success_message, cls="text-sm"),
cls="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4"
)
# Create user rows
user_rows = []
for user in users:
# Format dates
created_at = user.get('created_at', 'N/A')
if created_at and created_at != 'N/A':
created_at = created_at.split('T')[0] # Simple date format
last_login = user.get('last_login', 'Never')
if last_login and last_login != 'Never':
last_login = last_login.split('T')[0] # Simple date format
# Create user row
user_rows.append(
Tr(
Td(user['username'], cls="px-6 py-4 whitespace-nowrap"),
Td(user['email'], cls="px-6 py-4 whitespace-nowrap"),
Td(
Span(
"GitHub" if user.get('is_github_user') else "Email",
cls=f"px-2 py-1 text-xs rounded-full {'bg-purple-200 text-purple-800' if user.get('is_github_user') else 'bg-blue-200 text-blue-800'}"
),
cls="px-6 py-4 whitespace-nowrap"
),
Td(created_at, cls="px-6 py-4 whitespace-nowrap"),
Td(last_login, cls="px-6 py-4 whitespace-nowrap"),
Td(
Span(
"Admin" if user.get('is_admin') else "User",
cls=f"px-2 py-1 text-xs rounded-full {'bg-red-200 text-red-800' if user.get('is_admin') else 'bg-gray-200 text-gray-800'}"
),
cls="px-6 py-4 whitespace-nowrap"
),
Td(
Div(
# Toggle admin status
Form(
Button(
"Remove Admin" if user.get('is_admin') else "Make Admin",
type="submit",
cls=f"{'bg-gray-500 hover:bg-gray-600' if user.get('is_admin') else 'bg-blue-500 hover:bg-blue-600'} text-white text-xs py-1 px-2 rounded mr-2"
),
action=f"/admin/users/{user['id']}/{'remove-admin' if user.get('is_admin') else 'make-admin'}",
method="post",
cls="inline"
),
# Delete user
Form(
Button(
"Delete",
type="submit",
cls="bg-red-500 hover:bg-red-600 text-white text-xs py-1 px-2 rounded"
),
action=f"/admin/users/{user['id']}/delete",
method="post",
cls="inline"
),
cls="flex"
),
cls="px-6 py-4 whitespace-nowrap"
),
cls="bg-white border-b"
)
)
# Build pagination controls
current_page = page
pagination = Div(
Div(
A("← Previous",
href=f"/admin/users?page={current_page - 1}" if current_page > 1 else "#",
cls=f"px-4 py-2 rounded {'bg-blue-600 text-white' if current_page > 1 else 'bg-gray-200 text-gray-500 cursor-default'}"),
Span(f"Page {current_page}",
cls="px-4 py-2"),
A("Next →",
href=f"/admin/users?page={current_page + 1}" if len(users) == limit else "#",
cls=f"px-4 py-2 rounded {'bg-blue-600 text-white' if len(users) == limit else 'bg-gray-200 text-gray-500 cursor-default'}"),
cls="flex items-center justify-center space-x-2"
),
cls="mt-6"
)
return Div(
# Breadcrumb navigation
Div(
A("Admin Dashboard", href="/admin", cls="text-blue-600 hover:underline"),
Span(" / ", cls="text-gray-500"),
Span("Users", cls="font-semibold"),
cls="mb-4 text-sm"
),
# Page header
H1("User Management", cls="text-3xl font-bold text-gray-800 mb-6"),
# Message alert
message_alert if message_alert else "",
# Users table
Div(
Table(
Thead(
Tr(
Th("Username", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Email", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Auth Type", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Created", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Last Login", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Role", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
Th("Actions", cls="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"),
cls="bg-gray-50"
)
),
Tbody(
*user_rows if user_rows else [
Tr(
Td("No users found", colspan="7", cls="px-6 py-4 text-center text-gray-500 italic")
)
]
),
cls="min-w-full divide-y divide-gray-200"
),
cls="bg-white shadow overflow-x-auto rounded-lg"
),
# Pagination
pagination,
cls="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"
)

View File

@@ -0,0 +1,220 @@
import dataclasses
import uuid
from io import BytesIO
import pandas as pd
from fasthtml.components import *
from fasthtml.fastapp import fast_app
from starlette.datastructures import UploadFile
from components.datagrid.DataGrid import DataGrid, DG_FILTER_INPUT, DG_TABLE_FOOTER, DG_COLUMNS, DG_DATATYPE_STRING, \
DG_DATATYPE_BOOL, DG_READ_ONLY
from components.datagrid.constants import DG_ROWS_INDEXES
from core.settings_management import SettingsManager
from core.utils import make_html_id, make_column_id
ID_PREFIX = "import_settings"
import_settings_app, rt = fast_app()
_instances = {}
IMPORT_SETTINGS_PATH = "import-settings"
@dataclasses.dataclass
class ColumnDef:
index: int
title: str
header: str
position: str
@dataclasses.dataclass
class Config:
type: str = None
file_name: str = None
sheet_name: str = None
header_row: int = None
columns: list[ColumnDef] = dataclasses.field(default_factory=list)
class AdminImportSettings:
def __new__(cls, *args, **kwargs):
id_to_use = f"{ID_PREFIX}-{make_html_id(kwargs.get('id', None))}"
if id_to_use in _instances:
return _instances[id_to_use]
return super().__new__(cls)
def __init__(self, settings_manager: SettingsManager, config: Config = None, /, id=None):
if not hasattr(self, "_initialized"):
self._initialized = True
self._id = f"{ID_PREFIX}-{make_html_id(id) if id else uuid.uuid4().hex}"
_instances[self._id] = self
self.settings_manager = settings_manager
self.config = config or Config()
self.grid_settings = {
DG_COLUMNS: {
"column_id": {
"index": 1,
"title": "Column Id",
"type": DG_DATATYPE_STRING,
DG_READ_ONLY: False
},
"column_header": {
"index": 2,
"title": "Column Header",
"type": DG_DATATYPE_STRING,
},
"is_amount": {
"index": 3,
"title": "Amount",
"type": DG_DATATYPE_BOOL,
DG_READ_ONLY: False
},
},
DG_FILTER_INPUT: False,
DG_TABLE_FOOTER: False,
# DG_COLUMNS_REORDERING: False,
DG_ROWS_INDEXES: True,
}
self.datagrid = DataGrid(grid_settings=self.grid_settings)
self.content = None
self.sheet_names = None
def redraw(self):
return (
self._make_grid_component(),
self._make_select_sheet_name_component(sheet_names=self.sheet_names, selected=self.config.sheet_name, oob=True),
self._make_header_row_selection(header=self.config.header_row, oob=True),
)
def _make_excel_upload_component(self, oob=False):
return Input(type='file',
name='file',
hx_post=f"{IMPORT_SETTINGS_PATH}/upload",
hx_target=f"#dg_{self._id}", # select sheet_name
hx_encoding='multipart/form-data',
hx_swap="outerHTML",
hx_vals=f'{{"_id": "{self._id}"}}',
id=f"fu_{self._id}", # fu stands for 'file upload'
hx_swap_oob='true' if oob else None,
cls="file-input file-input-bordered file-input-sm w-full",
)
def _make_select_sheet_name_component(self, sheet_names=None, selected=None, oob=False):
options = [Option("No file selected", selected=True, disabled=True)] if sheet_names is None else \
[Option(
name,
selected=True if name == selected else None,
) for name in sheet_names]
return Select(
*options,
id=f"s_{self._id}",
name="sheet_name",
hx_post=f"{IMPORT_SETTINGS_PATH}/get-columns",
hx_include=f"#hr_{self._id}",
hx_target=f"#dg_{self._id}",
hx_vals=f'{{"_id": "{self._id}"}}',
hx_swap_oob='true' if oob else None,
cls="select select-bordered select-sm w-full"
)
def _make_header_row_selection(self, header=None, oob=False):
return Input(type="text",
name="header_row",
id=f"hr_{self._id}",
placeholder="Header row",
value=f"{header}",
hx_post=f"{IMPORT_SETTINGS_PATH}/get-columns",
hx_include=f"#s_{self._id}",
hx_target=f"#dg_{self._id}",
hx_vals=f'{{"_id": "{self._id}"}}',
hx_swap_oob='true' if oob else None,
cls="input input-bordered input-sm w-full",
)
def _make_grid_component(self, oob=False):
return Div(
self.datagrid,
id=f"dg_{self._id}",
hx_swap_oob='true' if oob else None,
)
def _make_use_columns_letters(self, oob=False):
return Label(
Span("Use columns letters", cls="label-text"),
Input(type="checkbox", cls="checkbox checkbox-sm", ),
cls="label cursor-pointer", )
def get_columns_def(self, columns):
res = []
_mapping = {
"Column Id": lambda c: make_column_id(str(c).strip()),
"Column Header": lambda c: c,
"Amount": lambda c: False,
}
for column in columns:
conf = {}
res.append(conf)
for col_id, col_conf in self.grid_settings[DG_COLUMNS].items():
col_title = col_conf["title"]
conf[col_title] = _mapping[col_title](column)
return res
def on_file_uploaded(self, file_name, content):
if content is None:
return None
self.content = content
excel_file = pd.ExcelFile(BytesIO(self.content))
sheet_names = excel_file.sheet_names
self.sheet_names = sheet_names
self.config.file_name = file_name
self.config.sheet_name = sheet_names[0]
self.config.header_row = 0
self.on_excel_conf_changed(self.config.sheet_name, self.config.header_row)
def on_excel_conf_changed(self, sheet_name, header):
self.config.sheet_name = sheet_name
self.config.header_row = header
if self.content is not None:
df = pd.read_excel(BytesIO(self.content), sheet_name=sheet_name, header=header)
columns_def = self.get_columns_def(df.columns)
new_content = pd.DataFrame(columns_def)
self.datagrid.import_dataframe(new_content, reset=False)
def __ft__(self):
return Div(
Div(self._make_excel_upload_component(), cls="col-span-2"),
Div(self._make_select_sheet_name_component(), cls="col-span-2"),
Div(self._make_header_row_selection(), cls="col-span-1"),
Div(self._make_use_columns_letters(), cls="col-span-1"),
cls="grid grid-cols-5 gap-2",
), Div(self._make_grid_component())
@rt("/upload")
async def post(session, _id: str, file: UploadFile):
try:
instance = _instances[_id]
content = await file.read()
instance.on_file_uploaded(file.filename, content)
return instance.redraw()
except ValueError as error:
return Div(f"{error}", role="alert", cls="alert alert-error")
@rt("/get-columns")
def post(session, _id: str, sheet_name: str, header_row: str):
print(f"sheet_name={sheet_name}, header_row={header_row}")
try:
instance = _instances[_id]
header = int(header_row) if header_row is not None else 0
instance.on_excel_conf_changed(sheet_name, header)
return instance.redraw()
except Exception as error:
return Div(f"{error}", role="alert", cls="alert alert-error")

15
src/pages/another_grid.py Normal file
View File

@@ -0,0 +1,15 @@
import pandas as pd
from components.datagrid.DataGrid import DataGrid
data = {
'Name': ['Kodjo', 'Kokoe', 'Aba', 'Koffi'],
'Age': [49, 51, 46, 51],
'City': ['Rosny', 'Nangis', 'Rosny', 'Abidjan']
}
df = pd.DataFrame(data)
def get_datagrid2():
return DataGrid(df, id="another grid")

98
src/pages/basic_test.py Normal file
View File

@@ -0,0 +1,98 @@
from fasthtml.common import *
from components.datagrid.icons import icon_filter_regular, icon_dismiss_regular
basic_test_app, rt = fast_app()
BASIC_TEST_PATH = "basic_test"
# def get_basic_test(): return Div(P('Hello World!'), hx_get=f"{BASIC_TEST_PATH}/change", hx_swap="outerHTML")
#
#
# @rt('/change')
# def get(): return Div(P('Nice to be here!'), hx_get=f"{BASIC_TEST_PATH}/change_again", hx_swap="outerHTML")
#
#
# @rt('/change_again')
# def get(): return Div(P('I changed again'), hx_get=f"{BASIC_TEST_PATH}/", hx_swap="outerHTML")
# def get_basic_test():
# icons = {"up": icon_chevron_sort_up,
# "down": icon_chevron_sort_down,
# "sort": icon_chevron_sort,
# "filter": icon_filter}
# return Div(
# Table(
# Thead(),
# Tbody(
# *[
# Tr(
# Td(name), Td(Div(icon, cls="icon-24")),
# ) for name, icon in icons.items()
# ]
# )
# )
# )
#
# def get_basic_test():
# return Div("Get Some HTML, Including A Value in the Request", hx_post="/example", hx_vals='{"myVal": "My Value"}')
after_request_attr = {"hx-on::after-request": f"console.log('after-request');"}
self_id = 'my_id'
def def_get_filter(oob=False):
def _inner_get_filter():
pass
return Div(
Label(Div(icon_filter_regular, cls="icon-24"),
Input(name='f',
placeholder="Filter...",
hx_post=f"/{BASIC_TEST_PATH}/filter",
hx_trigger="keyup changed delay:300ms",
hx_target=f"#tb_{self_id}",
hx_vals=f'{{"g_id": "{self_id}", "c_id":"{BASIC_TEST_PATH}"}}',
hx_swap_oob='true' if oob else None,
**after_request_attr,
),
cls="input input-bordered input-sm flex items-center gap-2"
),
id=f"f_{self_id}",
hx_swap_oob='true' if oob else None,
)
def get_component(oob=False):
return Div(
def_get_filter(),
Div(icon_dismiss_regular,
cls="icon-24 my-auto icon-btn ml-2",
hx_post=f"{BASIC_TEST_PATH}/reset_filter",
hx_trigger="click",
hx_target=f"#tb_{self_id}",
hx_vals=f'{{"g_id": "{self_id}", "c_id":"{BASIC_TEST_PATH}"}}',
**after_request_attr),
cls="flex mb-2",
id=f"fa_{self_id}", # fa stands for 'filter all'
hx_swap_oob='true' if oob else None,
)
def get_basic_test():
return get_component(), Div(id=f"tb_{self_id}")
@rt(f"/filter")
def post(f: str):
return Div(f)
@rt(f"/reset_filter")
def post():
res = (Div(f"You reset",
hx_swap_oob="true",
id=f"tb_{self_id}"
),
def_get_filter(oob=True))
return res,

134
src/pages/home.py Normal file
View File

@@ -0,0 +1,134 @@
from fasthtml.common import *
import config
from auth.auth_manager import AuthManager
def home(session=None):
"""
Defines the home page content.
Args:
session: The session object for auth status
Returns:
Components representing the home page content
"""
# Check if user is authenticated
is_authenticated = AuthManager.is_authenticated(session) if session else False
is_admin = AuthManager.is_admin(session) if session else False
username = session.get("username", "") if session else ""
# Hero content varies based on authentication
if is_authenticated:
hero_content = Div(
H1(f"Welcome back, {username}!",
cls="text-4xl font-bold text-center text-gray-800 mb-4"),
P("Continue creating engaging titles for your content with AI assistance.",
cls="text-xl text-center text-gray-600 mb-6"),
Div(
A("Generate New Titles",
href="/title-generator",
cls="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-3"),
A("View My History",
href="/history",
cls="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded"),
*([A("Admin Dashboard",
href="/admin",
cls="ml-3 bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded")] if is_admin else []),
cls="flex justify-center flex-wrap gap-y-2"
),
cls="py-12"
)
else:
hero_content = Div(
H1(config.APP_NAME,
cls="text-4xl font-bold text-center text-gray-800 mb-4"),
P("Create engaging titles for your content with AI assistance.",
cls="text-xl text-center text-gray-600 mb-6"),
Div(
A("Sign In",
href="/login",
cls="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-3"),
A("Register",
href="/register",
cls="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded"),
cls="flex justify-center"
),
cls="py-12"
)
return Div(
# Hero section with conditional content
hero_content,
# Features section
Div(
H2("Features", cls="text-3xl font-bold text-center mb-8"),
Div(
# Feature 1
Div(
H3("Platform-Specific", cls="text-xl font-semibold mb-2"),
P("Generate titles optimized for blogs, YouTube, social media, and more.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
# Feature 2
Div(
H3("Multiple Styles", cls="text-xl font-semibold mb-2"),
P("Choose from professional, casual, clickbait, or informative styles.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
# Feature 3
Div(
H3("AI-Powered", cls="text-xl font-semibold mb-2"),
P("Utilizes advanced AI models to craft engaging, relevant titles.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
cls="grid grid-cols-1 md:grid-cols-3 gap-6"
),
cls="py-8"
),
# How it works section
Div(
H2("How It Works", cls="text-3xl font-bold text-center mb-8"),
Div(
# Step 1
Div(
Div(
"1",
cls="flex items-center justify-center bg-blue-600 text-white text-xl font-bold rounded-full w-10 h-10 mb-4"
),
H3("Enter Your Topic", cls="text-xl font-semibold mb-2"),
P("Describe what your content is about in detail.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
# Step 2
Div(
Div(
"2",
cls="flex items-center justify-center bg-blue-600 text-white text-xl font-bold rounded-full w-10 h-10 mb-4"
),
H3("Choose Settings", cls="text-xl font-semibold mb-2"),
P("Select the platform and style that matches your needs.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
# Step 3
Div(
Div(
"3",
cls="flex items-center justify-center bg-blue-600 text-white text-xl font-bold rounded-full w-10 h-10 mb-4"
),
H3("Get Results", cls="text-xl font-semibold mb-2"),
P("Review multiple title options and choose your favorite.",
cls="text-gray-600"),
cls="bg-white p-6 rounded-lg shadow-md"
),
cls="grid grid-cols-1 md:grid-cols-3 gap-6"
),
cls="py-8"
)
)

View File

@@ -0,0 +1,67 @@
import pandas as pd
from components.datagrid.DataGrid import DataGrid, DG_COLUMNS, DG_READ_ONLY, DG_AGGREGATE_FILTERED_SUM, VISIBLE_KEY
from components.datagrid.constants import DG_ROWS_INDEXES
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'David', "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
'Age': [25, 30, 35, 40, 50],
'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', "bbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
'Boolean': [True, False, False, True, False],
'Choice': ["Bool", "Number", "Amount", None, "Number"]
}
df = pd.DataFrame(data)
grid_settings = {
# DG_READ_ONLY: False,
# DG_SELECTION_MODE: DG_SELECTION_MODE_ROW,
# DG_FILTER_INPUT: False,
# DG_TABLE_HEADER: False,
DG_ROWS_INDEXES: True,
DG_COLUMNS: {
"name": {
"index": 0,
"title": "Name",
# "type": "list",
"values": ["Alice", "Bob", "Charlie", "David"],
DG_READ_ONLY: False,
# "width": "150px",
},
"age": {
"index": 1,
"title": "Age",
"type": "number",
# "width": "100px",
VISIBLE_KEY: False,
"agg_func": DG_AGGREGATE_FILTERED_SUM,
# "agg_func_2": DG_AGGREGATE_SUM,
},
"city": {
"index": 2,
"title": "City",
"type": "list",
"values": ["New York", "Los Angeles", "Chicago", "Houston"],
DG_READ_ONLY: False,
},
"boolean": {
"index": 4,
"title": "Boolean",
"type": "bool",
# "width": "100px",
DG_READ_ONLY: False,
},
"choice": {
"index": 3,
"title": "Choice",
"type": "choice",
"values": ["Bool", "Number", "Amount"],
DG_READ_ONLY: False,
},
}
}
def get_datagrid():
dg = DataGrid(df, grid_settings=grid_settings, id="testing_datagrid0")
return dg.mk_excel_upload_component(), dg

View File

@@ -0,0 +1,16 @@
from fasthtml.common import *
def testing_restore_state():
return Div(
Input(name='f', id=f"input_with_id", placeholder="Input with id"),
Input(name='f', placeholder="Input without id"),
Input(type='checkbox', id="checkbox_with_id", checked='checked', cls='checkbox'),
Input(type='checkbox', checked='checked', cls='checkbox'),
Input(type='radio', id="radio_with_id_1", name='radio-1', cls='radio'),
Input(type='radio', id="radio_with_id_2", name='radio-1', cls='radio'),
Input(type='radio', id="radio_with_id_3", name='radio-1', cls='radio'),
)