summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 5eb00ab)
raw | patch | inline | side by side (parent: 5eb00ab)
author | Eric Wertz <ericwertz@Erics-MacBook-Pro.local> | |
Mon, 9 Jun 2025 04:11:51 +0000 (00:11 -0400) | ||
committer | Eric Wertz <ericwertz@Erics-MacBook-Pro.local> | |
Mon, 9 Jun 2025 04:11:51 +0000 (00:11 -0400) |
index 4aa284b2c6abc631fd71fd6f81367f7d3f5e7e82..3619be387009e2b38dc8024735cfad2852ae703a 100644 (file)
--- a/backend/HTTPRequest.cpp
+++ b/backend/HTTPRequest.cpp
_queryParams[param] = "";
}
}
+
+ _path = pathWithoutQuery;
}
// Parse path components
- std::istringstream pathStream(pathWithoutQuery);
+ std::istringstream pathStream(_path);
std::string component;
while (std::getline(pathStream, component, '/')) {
index 19e3f0e88cd999846b65a90bffa49c1e404addfc..1417e044d645816d2da1de52610d448dff50d0c0 100644 (file)
// CStaticFileResponse Implementation
CStaticFileResponse::CStaticFileResponse(const CHTTPRequest& request, std::string path) : CHTTPResponse(request)
{
- // Remove query string from path for routing decisions
- std::string clean_path = path;
- size_t query_pos = clean_path.find('?');
- if (query_pos != std::string::npos) {
- clean_path = clean_path.substr(0, query_pos);
- }
-
// Index is easy enough to handle
- if (clean_path == "/" )
+ if (path == "/" )
{
serveFileFromPath("index.html");
return;
{
// Remove query string if present
std::string path = requested_path;
- size_t query_pos = path.find('?');
- if (query_pos != std::string::npos) {
- path = path.substr(0, query_pos);
- }
// Basic path normalization - remove duplicate slashes and handle relative paths
std::string normalized = "/";
index 9a56c34b03eff73f3ac1cc7a959f682085001fcb..868e85be55e8ab242c6a0502ab46a6e1e063cfa7 100644 (file)
import NotFound from 'views/NotFound';
// Wrapper component to handle auth checks
-const AuthRoute = ({ component: Component, auth_required }: { component: ComponentType, auth_required: boolean }) => {
+const AuthRoute = ({ component: Component, auth_required, title }: { component: ComponentType, auth_required: boolean, title: string }) => {
const { isLoading, loggedIn, checkAuth } = useContext(AppContext);
const [, route] = useRouter();
}
}, [isLoading, loggedIn, auth_required, route]);
+ // Set page title
+ useEffect(() => {
+ document.title = title;
+ }, [title]);
+
if (isLoading) {
return null;
}
<AuthRoute
component={route.view}
auth_required={route.auth_required}
+ title={route.title}
/>
)}
/>
index 70380e0fd3b7b17688e5dede1d62575dec32ecbc..d6da90bf5e84075268b4a042dd5ae1711148198c 100644 (file)
--- a/frontend/app_routes.tsx
+++ b/frontend/app_routes.tsx
{
path: '/',
view: List,
+ title: 'List',
auth_required: true,
},
{
path: '/list',
view: List,
+ title: 'List',
auth_required: true,
},
{
path: '/signup',
view: SignUp,
+ title: 'Sign Up',
auth_required: false,
},
{
path: '/login',
view: Login,
+ title: 'Login',
auth_required: false,
},
];
index 0c2659a7e19540124e8295b3f2ef9e198440db54..c26a02e27fb61b98d00e75b3e1a357b8e44d62de 100644 (file)
const previousItem = findPreviousItem(prevItems, currentItem._uuid);
if (!previousItem) return prevItems;
- // Update previous item and mark current as deleted
+ // Get the children of the item being deleted
+ const childrenToMove = currentItem._children.filter(child => !child.__is_deleted);
+
+ // Update previous item with remaining content and append deleted item's children
let result = updateItemInTree(prevItems, previousItem._uuid, item => ({
...item,
content: item.content + remainingContent,
+ _children: reorderItems([...item._children, ...childrenToMove]),
__is_dirty: true
}));
index af4f250159a7504da2a07184a8c205c44d3db517..f9247ed412a1e1bfee1b2b65e643d655fe18b4ee 100644 (file)
@@ -101,14 +101,26 @@ export const ListItem = forwardRef<ListItemRef, ListItemProps>(({ item, onChange
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
- // Enter key - create new item
- event.preventDefault();
- const target = event.target as HTMLDivElement;
- const cursorPosition = getCursorPosition(target);
- const fullContent = target.textContent || '';
- const beforeContent = fullContent.substring(0, cursorPosition);
- const afterContent = fullContent.substring(cursorPosition);
- createItem(item, beforeContent, afterContent);
+ if (event.shiftKey === true) {
+ // Shift+Enter - add newline (allow default behavior, then update content)
+ setTimeout(() => {
+ const target = event.target as HTMLDivElement;
+ const content = target.innerHTML;
+ if (item.content !== content) {
+ onChange({ ...item, content, __is_dirty: true });
+ }
+ }, 0);
+ return;
+ } else {
+ // Enter key - create new item
+ event.preventDefault();
+ const target = event.target as HTMLDivElement;
+ const cursorPosition = getCursorPosition(target);
+ const fullContent = target.textContent || '';
+ const beforeContent = fullContent.substring(0, cursorPosition);
+ const afterContent = fullContent.substring(cursorPosition);
+ createItem(item, beforeContent, afterContent);
+ }
} else if (event.key === 'Backspace') {
const target = event.target as HTMLDivElement;
const cursorPosition = getCursorPosition(target);
index f1227c672945b17b639786a41afdd838eaedda28..4a3e89f3e022ce6755a660aa2cdbc54e9ff4e3f4 100644 (file)
import { h, JSX } from 'preact';
-import { useContext } from 'preact/hooks';
+import { useContext, useEffect, useRef } from 'preact/hooks';
import { AppContext } from 'app/AppContext';
import LoadingSpinner from './LoadingSpinner';
import { MenuIcon, CheckIcon, CircleIcon } from './Icons';
export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => {
const appContext = useContext(AppContext);
+ const toolbarRef = useRef<HTMLDivElement>(null);
const handleMenuClick = () => {
if (onMenuClick) {
}
};
+ useEffect(() => {
+ function resizeHandler() {
+ if( !toolbarRef.current) {
+ return;
+ }
+
+ if( window.visualViewport?.width && window.visualViewport.width > 480) {
+ return;
+ }
+
+ // viewport height
+ const viewportHeight = window.visualViewport?.height ?? 0;
+ const offsetTop = window.visualViewport?.offsetTop ?? 0;
+ const viewportScale = window.visualViewport?.scale ?? 1;
+ // math - removed scale transform to prevent event handling issues
+ toolbarRef.current.style.transform = `translate( 0px, ${viewportHeight - toolbarRef.current.offsetHeight + offsetTop}px)`;
+ }
+
+ // run first time to initialize
+ resizeHandler();
+
+ // subscribe to events which affect scroll, or viewport position
+ window.visualViewport?.addEventListener('resize', resizeHandler);
+ window.visualViewport?.addEventListener('scroll', resizeHandler);
+ window?.addEventListener('touchmove', resizeHandler);
+
+ // unsubscribe
+ return () => {
+ window.visualViewport?.removeEventListener('resize', resizeHandler);
+ window.visualViewport?.removeEventListener('scroll', resizeHandler);
+ window?.removeEventListener('touchmove', resizeHandler);
+ };
+ }, []);
+
return (
- <div class="toolbar">
- <div class="toolbar-left">
- <ToolbarButton
- icon={<MenuIcon size={20} />}
- onClick={handleMenuClick}
- title="Menu"
- menuOptions={[
- {
- label: 'List',
- onClick: () => {
- window.location.href = '/list';
+ <div class="toolbar-spacer">
+ <div class="toolbar" ref={toolbarRef}>
+ <div class="toolbar-left">
+ <ToolbarButton
+ icon={<MenuIcon size={20} />}
+ onClick={handleMenuClick}
+ title="Menu"
+ menuOptions={[
+ {
+ label: 'List',
+ onClick: () => {
+ window.location.href = '/list';
+ }
+ },
+ {
+ label: 'Chat',
+ onClick: () => {
+ window.location.href = '/chat';
+ }
+ },
+ {
+ label: 'Logout',
+ onClick: () => {
+ appContext.logout();
+ }
}
- },
- {
- label: 'Chat',
- onClick: () => {
- window.location.href = '/chat';
- }
- },
- {
- label: 'Logout',
- onClick: () => {
- appContext.logout();
- }
- }
- ]}
- />
- <div class="toolbar-separator" />
- {buttons.map((button, index) => (
- <div key={index} class="toolbar-button-container">
- <ToolbarButton
- icon={button.icon}
- onClick={button.onClick}
- title={button.title}
- disabled={button.disabled}
- active={button.active}
- />
- {button.separatorAfter && (
- <div class="toolbar-separator" />
- )}
- </div>
- ))}
- </div>
-
- <div class="toolbar-right">
- {appContext.saveStatus === 'saving' ? (
- <LoadingSpinner />
- ) : appContext.saveStatus === 'saved' ? (
- <CheckIcon size={20} />
- ) : appContext.saveStatus === 'queued' ? (
- <CircleIcon size={20} />
- ) : (
- <CheckIcon size={20} />
- )}
+ ]}
+ />
+ <div class="toolbar-separator" />
+ {buttons.map((button, index) => (
+ <div key={index} class="toolbar-button-container">
+ <ToolbarButton
+ icon={button.icon}
+ onClick={button.onClick}
+ title={button.title}
+ disabled={button.disabled}
+ active={button.active}
+ />
+ {button.separatorAfter && (
+ <div class="toolbar-separator" />
+ )}
+ </div>
+ ))}
+ </div>
+
+ <div class="toolbar-right">
+ {appContext.saveStatus === 'saving' ? (
+ <LoadingSpinner />
+ ) : appContext.saveStatus === 'saved' ? (
+ <CheckIcon size={20} />
+ ) : appContext.saveStatus === 'queued' ? (
+ <CircleIcon size={20} />
+ ) : (
+ <CheckIcon size={20} />
+ )}
+ </div>
</div>
</div>
);
diff --git a/frontend/styles.css b/frontend/styles.css
index af21cbdf8106bae5b4ba884dba2fe4b19676fbae..62fbc71916efcbf1923bd41418018f0a2d328d10 100644 (file)
--- a/frontend/styles.css
+++ b/frontend/styles.css
/* Base Styles */
body {
font-family: 'Lato', sans-serif;
+ padding-bottom: 3.25rem; /* Account for fixed bottom toolbar */
+ margin: 0;
+ min-height: 100vh;
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
h1 {
}
.loader {
- width: 48px;
- height: 48px;
+ width: 3rem;
+ height: 3rem;
border-radius: 50%;
position: relative;
- animation: rotate 1s linear infinite
- }
+ animation: rotate 1s linear infinite;
+}
+
.loader-small {
- width: 24px;
- height: 24px;
+ width: 1.5rem;
+ height: 1.5rem;
border-radius: 50%;
position: relative;
- animation: rotate 1s linear infinite
- }
- .loader::before, .loader-small::before {
+ animation: rotate 1s linear infinite;
+}
+
+.loader::before, .loader-small::before {
content: "";
box-sizing: border-box;
position: absolute;
- inset: 0px;
+ inset: 0;
border-radius: 50%;
- border: 5px solid #0074ff;
- animation: prixClipFix 2s linear infinite ;
- }
-
- @keyframes rotate {
- 100% {transform: rotate(360deg)}
- }
-
- @keyframes prixClipFix {
- 0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)}
- 25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)}
- 50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)}
- 75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)}
- 100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)}
- }
+ border: 0.3125rem solid #0074ff;
+ animation: prixClipFix 2s linear infinite;
+}
+
+@keyframes rotate {
+ 100% { transform: rotate(360deg) }
+}
+
+@keyframes prixClipFix {
+ 0% { clip-path: polygon(50% 50%,0 0,0 0,0 0,0 0,0 0) }
+ 25% { clip-path: polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0) }
+ 50% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%) }
+ 75% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%) }
+ 100% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0) }
+}
/* Form Styles */
form {
- max-width: 400px;
+ max-width: 25rem;
margin: 2rem auto;
padding: 2rem;
background: white;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ border-radius: 0.5rem;
+ box-shadow: 0 0.125rem 0.625rem rgba(0, 0, 0, 0.1);
}
form h1 {
form input {
width: 100%;
- padding: 12px;
+ padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
- border-radius: 4px;
+ border-radius: 0.25rem;
font-size: 1rem;
transition: border-color 0.3s ease;
}
form input:focus {
outline: none;
border-color: #0074ff;
- box-shadow: 0 0 0 2px rgba(0, 116, 255, 0.1);
+ box-shadow: 0 0 0 0.125rem rgba(0, 116, 255, 0.1);
}
form button {
width: 100%;
- padding: 12px;
+ padding: 0.75rem;
background: #0074ff;
color: white;
border: none;
- border-radius: 4px;
+ border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
animation: slideDown 0.3s ease forwards;
z-index: 2000;
border: 1px solid #000;
- border-radius: 4px;
+ border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
- backdrop-filter: blur(10px);
+ backdrop-filter: blur(0.625rem);
}
.status-bar.info {
}
@keyframes slideDown {
- from {
- transform: translateY(-100%);
- }
- to {
- transform: translateY(0);
- }
+ from { transform: translateY(-100%); }
+ to { transform: translateY(0); }
}
@keyframes slideUp {
- from {
- transform: translateY(0);
- }
- to {
- transform: translateY(-100%);
- }
+ from { transform: translateY(0); }
+ to { transform: translateY(-100%); }
}
/* List Item Styles */
transition: all 0.2s ease;
min-height: 1.2em;
outline: none;
- display: inline-block;
+ display: block;
}
li > div[contenteditable]:focus {
/* Indentation levels using CSS custom properties */
li {
- --indent-size: 24px;
+ --indent-size: 1.5rem;
}
.list-item-level-0 {
font-style: italic;
}
+/* Toolbar Styles */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 8px 16px;
+ padding: 0.5rem 1rem;
background: linear-gradient(135deg, rgba(248, 249, 250, 0.85) 0%, rgba(233, 236, 239, 0.85) 100%);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(0.5rem);
+ -webkit-backdrop-filter: blur(0.5rem);
border-bottom: 1px solid #dee2e6;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
- height: 52px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ height: 3.25rem;
+ box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.1);
+ -webkit-overflow-scrolling: touch;
}
-.toolbar-left {
- display: flex;
- align-items: center;
- gap: 8px;
+.toolbar-spacer {
+ display: block;
+ height: 3.75rem;
}
+.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 0.5rem;
}
.toolbar-button-container {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 0.5rem;
}
.toolbar-separator {
width: 1px;
- height: 24px;
+ height: 1.5rem;
background: #dee2e6;
- margin: 0 8px;
+ margin: 0 0.5rem;
}
/* Toolbar Button Wrapper for Menu Functionality */
display: flex;
align-items: center;
justify-content: center;
- width: 40px;
- height: 40px;
+ width: 2.5rem;
+ height: 2.5rem;
border: 1px solid transparent;
- border-radius: 6px;
+ border-radius: 0.375rem;
background: transparent;
color: #495057;
cursor: pointer;
/* Menu indicator for buttons with menus */
.toolbar-button.has-menu {
- padding-right: 18px;
+ padding-right: 1.125rem;
width: auto;
- min-width: 40px;
+ min-width: 2.5rem;
}
.menu-indicator {
position: absolute;
- right: 4px;
+ right: 0.25rem;
top: 50%;
transform: translateY(-50%);
- font-size: 8px;
+ font-size: 0.5rem;
line-height: 1;
opacity: 0.7;
pointer-events: none;
position: absolute;
top: 100%;
left: 0;
- min-width: 180px;
+ min-width: 11.25rem;
background: white;
border: 1px solid #dee2e6;
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ border-radius: 0.375rem;
+ box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15);
z-index: 1001;
- margin-top: 4px;
- padding: 4px 0;
- backdrop-filter: blur(10px);
+ margin-top: 0.25rem;
+ padding: 0.25rem 0;
+ backdrop-filter: blur(0.625rem);
animation: menuFadeIn 0.15s ease-out;
}
@keyframes menuFadeIn {
from {
opacity: 0;
- transform: translateY(-8px);
+ transform: translateY(-0.5rem);
}
to {
opacity: 1;
display: flex;
align-items: center;
width: 100%;
- padding: 8px 12px;
+ padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: #495057;
- font-size: 14px;
+ font-size: 0.875rem;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
- gap: 8px;
+ gap: 0.5rem;
}
.toolbar-menu-item:hover:not(.disabled) {
display: flex;
align-items: center;
justify-content: center;
- width: 16px;
- height: 16px;
+ width: 1rem;
+ height: 1rem;
flex-shrink: 0;
}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-}
\ No newline at end of file
+}
+
+/* Mobile and Responsive Styles */
+@media (max-width: 768px) {
+ .toolbar {
+ height: 2.75rem;
+ padding: 0.375rem 0.75rem;
+ }
+
+ .toolbar-spacer {
+ height: 3.0rem;
+ }
+
+ .toolbar-left,
+ .toolbar-right,
+ .toolbar-button-container {
+ gap: 0.375rem;
+ }
+
+ .toolbar-separator {
+ height: 1.25rem;
+ margin: 0 0.375rem;
+ }
+
+ .toolbar-button {
+ width: 2.25rem;
+ height: 2.25rem;
+ border-radius: 0.25rem;
+ }
+
+ .toolbar-button.has-menu {
+ padding-right: 1rem;
+ min-width: 2.25rem;
+ }
+
+ .menu-indicator {
+ right: 0.1875rem;
+ font-size: 0.4375rem;
+ }
+
+ .toolbar-menu {
+ min-width: 10rem;
+ margin-top: 0.1875rem;
+ }
+
+ .toolbar-menu-item {
+ padding: 0.375rem 0.625rem;
+ font-size: 0.8125rem;
+ gap: 0.375rem;
+ }
+
+ .menu-item-icon {
+ width: 0.875rem;
+ height: 0.875rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .toolbar {
+ height: 2.5rem;
+ padding: 0.25rem 0.5rem;
+ /*top: auto;
+ bottom: 0;*/
+ }
+
+ .toolbar-spacer {
+ height: 0;
+ }
+
+ .toolbar-left,
+ .toolbar-right,
+ .toolbar-button-container {
+ gap: 0.25rem;
+ }
+
+ .toolbar-separator {
+ height: 1.125rem;
+ margin: 0 0.25rem;
+ }
+
+ .toolbar-button {
+ width: 2rem;
+ height: 2rem;
+ }
+
+ .toolbar-button.has-menu {
+ padding-right: 0.875rem;
+ min-width: 2rem;
+ }
+
+ .menu-indicator {
+ right: 0.125rem;
+ font-size: 0.375rem;
+ }
+
+ .toolbar-menu {
+ min-width: 8.75rem;
+ margin-top: 0.125rem;
+ padding: 0.125rem 0;
+ }
+
+ .toolbar-menu-item {
+ padding: 0.3125rem 0.5rem;
+ font-size: 0.75rem;
+ gap: 0.25rem;
+ }
+
+ .menu-item-icon {
+ width: 0.75rem;
+ height: 0.75rem;
+ }
+}
index 3bdc4bd3fb47ed467e9a98c471deff7bf52d24b6..f06b3a8ab7812aa16e9fc77af773b8408ee06a54 100644 (file)
--- a/frontend/views/List.tsx
+++ b/frontend/views/List.tsx
return (
<div>
<Toolbar buttons={buttons} />
- <div style={{ marginTop: '72px' }}>
+ <div>
<ul>
{listContext.items.filter(item => !item.__is_deleted).map(item => (
<ListItem