working on making nojs workable

This commit is contained in:
Michael Freno
2025-12-22 15:10:13 -05:00
parent b640099fc5
commit 8f7b4cb6ea
12 changed files with 1342 additions and 24 deletions

View File

@@ -230,6 +230,35 @@ body {
transition: background-color 500ms ease-in-out;
}
[data-typewriter="animated"] [data-char-index] {
opacity: 0;
}
[data-typewriter-ready="true"] [data-char-index] {
transition: opacity 0.05s ease-in;
}
.bg-base.relative.h-screen {
width: 100vw;
margin-left: 0;
}
@media (min-width: 768px) {
.bg-base.relative.h-screen {
width: calc(100vw - 600px);
margin-left: 300px;
}
}
@media (max-width: 767px) {
nav.fixed.z-50[class*="border-r-2"] {
/* Left sidebar starts off-screen on mobile */
transform: translateX(-100%);
}
}
/* Note: JS will add inline styles and reactive classList that override these defaults */
.cursor-typing {
display: inline-block;
width: 2px;
@@ -1204,3 +1233,62 @@ svg.mermaid text {
.reference-item > span.ml-2 {
font-style: italic;
}
/* Conditional Block Styling in Editor */
.ProseMirror .conditional-block {
border: 2px dashed rgba(69, 112, 122, 0.5);
border-radius: 4px;
padding: 12px;
margin: 8px 0;
position: relative;
background: rgba(69, 112, 122, 0.05);
}
.ProseMirror .conditional-block::before {
content: "🔒 " attr(data-condition-type) ": " attr(data-condition-value);
position: absolute;
top: -12px;
left: 8px;
background: var(--color-blue);
color: var(--color-base);
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
z-index: 1;
}
.ProseMirror .conditional-block[data-show-when="false"]::before {
content: "🔒 NOT " attr(data-condition-type) ": " attr(data-condition-value);
}
.ProseMirror .conditional-content {
position: relative;
}
/* Inline conditional styling */
.ProseMirror .conditional-inline {
display: inline;
background: rgba(69, 112, 122, 0.15);
border-bottom: 2px dotted rgba(69, 112, 122, 0.6);
padding: 2px 4px;
border-radius: 3px;
position: relative;
cursor: pointer;
}
.ProseMirror .conditional-inline::after {
content: "🔒";
font-size: 10px;
margin-left: 2px;
opacity: 0.7;
}
.ProseMirror .conditional-inline[data-show-when="false"] {
background: rgba(193, 74, 74, 0.15);
border-bottom-color: rgba(193, 74, 74, 0.6);
}
.ProseMirror .conditional-inline[data-show-when="false"]::after {
content: "🔒❌";
}

View File

@@ -5,8 +5,7 @@ import {
ErrorBoundary,
Suspense,
onMount,
onCleanup,
Show
onCleanup
} from "solid-js";
import "./app.css";
import { LeftBar, RightBar } from "./components/Bars";
@@ -191,16 +190,18 @@ function AppLayout(props: { children: any }) {
<LeftBar />
<div
class="bg-base relative h-screen overflow-x-hidden overflow-y-scroll py-16"
style={{
width: `${centerWidth()}px`,
"margin-left": `${leftBarSize()}px`
}}
style={
barsInitialized()
? {
width: `${centerWidth()}px`,
"margin-left": `${leftBarSize()}px`
}
: undefined
}
onMouseUp={handleCenterTapRelease}
onTouchEnd={handleCenterTapRelease}
>
<Show when={barsInitialized()} fallback={<TerminalSplash />}>
<Suspense fallback={<TerminalSplash />}>{props.children}</Suspense>
</Show>
<Suspense fallback={<TerminalSplash />}>{props.children}</Suspense>
</div>
<RightBar />
</div>

View File

@@ -13,12 +13,13 @@ export function Typewriter(props: {
const [isTyping, setIsTyping] = createSignal(false);
const [isDelaying, setIsDelaying] = createSignal(delay > 0);
const [shouldHide, setShouldHide] = createSignal(false);
const [animated, setAnimated] = createSignal(false);
const resolved = children(() => props.children);
onMount(() => {
if (!containerRef || !cursorRef) return;
// FIRST: Walk DOM and hide all text immediately
// FIRST: Walk DOM and split text into character spans
const textNodes: { node: Text; text: string; startIndex: number }[] = [];
let totalChars = 0;
@@ -38,7 +39,7 @@ export function Typewriter(props: {
text.split("").forEach((char, i) => {
const charSpan = document.createElement("span");
charSpan.textContent = char;
charSpan.style.opacity = "0";
// Don't set opacity here - CSS will handle it based on data-typewriter state
charSpan.setAttribute(
"data-char-index",
String(totalChars - text.length + i)
@@ -54,6 +55,12 @@ export function Typewriter(props: {
walkDOM(containerRef);
// Mark as animated AFTER DOM manipulation - this triggers CSS to hide characters
setAnimated(true);
// Mark container as ready for animation
containerRef.setAttribute("data-typewriter-ready", "true");
// Position cursor at the first character location
const firstChar = containerRef.querySelector(
'[data-char-index="0"]'
@@ -143,7 +150,11 @@ export function Typewriter(props: {
};
return (
<div ref={containerRef} class={props.class}>
<div
ref={containerRef}
class={props.class}
data-typewriter={!animated() ? "static" : "animated"}
>
{resolved()}
<span ref={cursorRef} class={getCursorClass()}></span>
</div>

View File

@@ -16,6 +16,8 @@ import DetailsContent from "@tiptap/extension-details-content";
import { Node } from "@tiptap/core";
import { createLowlight, common } from "lowlight";
import { Mermaid } from "./extensions/Mermaid";
import { ConditionalBlock } from "./extensions/ConditionalBlock";
import { ConditionalInline } from "./extensions/ConditionalInline";
import TextAlign from "@tiptap/extension-text-align";
import Superscript from "@tiptap/extension-superscript";
import Subscript from "@tiptap/extension-subscript";
@@ -380,6 +382,24 @@ export default function TextEditor(props: TextEditorProps) {
const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false);
const [showConditionalConfig, setShowConditionalConfig] = createSignal(false);
const [conditionalConfigPosition, setConditionalConfigPosition] =
createSignal({
top: 0,
left: 0
});
const [conditionalForm, setConditionalForm] = createSignal<{
conditionType: "auth" | "privilege" | "date" | "feature" | "env";
conditionValue: string;
showWhen: "true" | "false";
inline: boolean; // New field for inline vs block
}>({
conditionType: "auth",
conditionValue: "authenticated",
showWhen: "true",
inline: false
});
const [isFullscreen, setIsFullscreen] = createSignal(false);
const [keyboardVisible, setKeyboardVisible] = createSignal(false);
const [keyboardHeight, setKeyboardHeight] = createSignal(0);
@@ -434,6 +454,8 @@ export default function TextEditor(props: TextEditorProps) {
}
}),
Mermaid,
ConditionalBlock,
ConditionalInline,
TextAlign.configure({
types: ["heading", "paragraph"],
alignments: ["left", "center", "right", "justify"],
@@ -1005,6 +1027,27 @@ export default function TextEditor(props: TextEditorProps) {
}
});
// Close conditional config on outside click
createEffect(() => {
if (showConditionalConfig()) {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
!target.closest(".conditional-config") &&
!target.closest("[data-conditional-trigger]")
) {
setShowConditionalConfig(false);
}
};
setTimeout(() => {
document.addEventListener("click", handleClickOutside);
}, 0);
return () => document.removeEventListener("click", handleClickOutside);
}
});
const showMermaidSelector = (e: MouseEvent) => {
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setMermaidMenuPosition({
@@ -1022,6 +1065,108 @@ export default function TextEditor(props: TextEditorProps) {
setShowMermaidTemplates(false);
};
// Conditional block functions
const showConditionalConfigurator = (e: MouseEvent) => {
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setConditionalConfigPosition({
top: buttonRect.bottom + 5,
left: buttonRect.left
});
// If cursor is in existing conditional, load its values
const instance = editor();
if (instance?.isActive("conditionalBlock")) {
const attrs = instance.getAttributes("conditionalBlock");
setConditionalForm({
conditionType: attrs.conditionType || "auth",
conditionValue: attrs.conditionValue || "authenticated",
showWhen: attrs.showWhen || "true",
inline: false
});
} else if (instance?.isActive("conditionalInline")) {
const attrs = instance.getAttributes("conditionalInline");
setConditionalForm({
conditionType: attrs.conditionType || "auth",
conditionValue: attrs.conditionValue || "authenticated",
showWhen: attrs.showWhen || "true",
inline: true
});
} else {
// Reset to defaults for new conditional
setConditionalForm({
conditionType: "auth",
conditionValue: "authenticated",
showWhen: "true",
inline: false
});
}
setShowConditionalConfig(!showConditionalConfig());
};
const insertConditionalBlock = () => {
const instance = editor();
if (!instance) return;
const { conditionType, conditionValue, showWhen, inline } =
conditionalForm();
if (inline) {
// Handle inline conditionals (Mark)
if (instance.isActive("conditionalInline")) {
// Update existing inline conditional
instance
.chain()
.focus()
.unsetConditionalInline()
.setConditionalInline({
conditionType,
conditionValue,
showWhen
})
.run();
} else {
// Apply inline conditional to selection
instance
.chain()
.focus()
.setConditionalInline({
conditionType,
conditionValue,
showWhen
})
.run();
}
} else {
// Handle block conditionals (Node)
if (instance.isActive("conditionalBlock")) {
// Update existing conditional
instance
.chain()
.focus()
.updateConditionalBlock({
conditionType,
conditionValue,
showWhen
})
.run();
} else {
// Wrap selection in new conditional
instance
.chain()
.focus()
.setConditionalBlock({
conditionType,
conditionValue,
showWhen
})
.run();
}
}
setShowConditionalConfig(false);
};
// Toggle fullscreen mode
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen());
@@ -1123,6 +1268,200 @@ export default function TextEditor(props: TextEditorProps) {
);
};
// Conditional Configurator Component
const ConditionalConfigurator = () => {
return (
<div class="bg-mantle border-surface2 w-80 rounded border p-4 shadow-lg">
<h3 class="text-text mb-3 font-semibold">Conditional Block</h3>
{/* Condition Type Selector */}
<label class="text-subtext0 mb-2 block text-xs">Condition Type</label>
<select
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionType}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionType: e.currentTarget.value as any
})
}
>
<option value="auth">User Authentication</option>
<option value="privilege">Privilege Level</option>
<option value="date">Date Range</option>
<option value="feature">Feature Flag</option>
<option value="env">Environment Variable</option>
</select>
{/* Dynamic Condition Value Input based on type */}
<Show when={conditionalForm().conditionType === "auth"}>
<label class="text-subtext0 mb-2 block text-xs">User State</label>
<select
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionValue}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionValue: e.currentTarget.value
})
}
>
<option value="authenticated">Authenticated</option>
<option value="anonymous">Anonymous</option>
</select>
</Show>
<Show when={conditionalForm().conditionType === "privilege"}>
<label class="text-subtext0 mb-2 block text-xs">
Privilege Level
</label>
<select
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionValue}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionValue: e.currentTarget.value
})
}
>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="anonymous">Anonymous</option>
</select>
</Show>
<Show when={conditionalForm().conditionType === "date"}>
<label class="text-subtext0 mb-2 block text-xs">Date Condition</label>
<input
type="text"
placeholder="before:2026-01-01 or after:2025-01-01"
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionValue}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionValue: e.currentTarget.value
})
}
/>
<div class="text-subtext0 mb-3 text-xs">
Format: before:YYYY-MM-DD, after:YYYY-MM-DD, or
between:YYYY-MM-DD,YYYY-MM-DD
</div>
</Show>
<Show when={conditionalForm().conditionType === "feature"}>
<label class="text-subtext0 mb-2 block text-xs">
Feature Flag Name
</label>
<input
type="text"
placeholder="feature-name"
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionValue}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionValue: e.currentTarget.value
})
}
/>
</Show>
<Show when={conditionalForm().conditionType === "env"}>
<label class="text-subtext0 mb-2 block text-xs">
Environment Variable
</label>
<input
type="text"
list="env-variables"
placeholder="NODE_ENV:production"
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().conditionValue}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
conditionValue: e.currentTarget.value
})
}
/>
<datalist id="env-variables">
<option value="NODE_ENV:development">
Development environment
</option>
<option value="NODE_ENV:production">Production environment</option>
<option value="NODE_ENV:test">Test environment</option>
<option value="VERCEL_ENV:preview">
Vercel preview deployment
</option>
<option value="VERCEL_ENV:production">Vercel production</option>
<option value="VITE_DOMAIN:*">Any domain configured</option>
<option value="VITE_AWS_BUCKET_STRING:*">
S3 bucket configured
</option>
<option value="VITE_GOOGLE_CLIENT_ID:*">Google auth enabled</option>
<option value="VITE_GITHUB_CLIENT_ID:*">GitHub auth enabled</option>
<option value="VITE_WEBSOCKET:*">WebSocket configured</option>
</datalist>
<div class="text-subtext0 mb-3 text-xs">
Format: VAR_NAME:value or VAR_NAME:* for any truthy value
</div>
</Show>
{/* Show When Toggle */}
<label class="text-subtext0 mb-2 block text-xs">Show When</label>
<select
class="bg-surface0 text-text border-surface2 mb-3 w-full rounded border px-2 py-1"
value={conditionalForm().showWhen}
onInput={(e) =>
setConditionalForm({
...conditionalForm(),
showWhen: e.currentTarget.value as "true" | "false"
})
}
>
<option value="true">Condition is TRUE</option>
<option value="false">Condition is FALSE</option>
</select>
{/* Inline Toggle */}
<label class="text-subtext0 mb-3 flex items-center gap-2 text-xs">
<input
type="checkbox"
checked={conditionalForm().inline}
onChange={(e) =>
setConditionalForm({
...conditionalForm(),
inline: e.currentTarget.checked
})
}
class="rounded"
/>
<span>Inline (no line break)</span>
</label>
{/* Action Buttons */}
<div class="flex gap-2">
<button
type="button"
onClick={insertConditionalBlock}
class="bg-blue rounded px-3 py-1 text-sm hover:brightness-125"
>
Apply
</button>
<button
type="button"
onClick={() => setShowConditionalConfig(false)}
class="hover:bg-surface1 rounded px-3 py-1 text-sm"
>
Cancel
</button>
</div>
</div>
);
};
return (
<div
ref={containerRef}
@@ -1475,6 +1814,19 @@ export default function TextEditor(props: TextEditorProps) {
</div>
</Show>
{/* Conditional Configurator */}
<Show when={showConditionalConfig()}>
<div
class="conditional-config fixed z-110"
style={{
top: `${conditionalConfigPosition().top}px`,
left: `${conditionalConfigPosition().left}px`
}}
>
<ConditionalConfigurator />
</div>
</Show>
{/* Main Toolbar - Pinned at top in fullscreen */}
<div
class="border-surface2 bg-base border-b"
@@ -1782,6 +2134,19 @@ export default function TextEditor(props: TextEditorProps) {
>
📊 Diagram
</button>
<button
type="button"
onClick={showConditionalConfigurator}
data-conditional-trigger
class={`${
instance().isActive("conditionalBlock")
? "bg-surface2"
: "hover:bg-surface1"
} rounded px-2 py-1 text-xs`}
title="Insert Conditional Block"
>
🔒 Conditional
</button>
<div class="border-surface2 mx-1 border-l"></div>
<button
type="button"

View File

@@ -0,0 +1,103 @@
import { Node, mergeAttributes } from "@tiptap/core";
export interface ConditionalBlockOptions {
HTMLAttributes: Record<string, any>;
}
export interface ConditionalBlockAttributes {
conditionType: "auth" | "privilege" | "date" | "feature" | "env";
conditionValue: string;
showWhen: "true" | "false";
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
conditionalBlock: {
setConditionalBlock: (
attributes: ConditionalBlockAttributes
) => ReturnType;
updateConditionalBlock: (
attributes: Partial<ConditionalBlockAttributes>
) => ReturnType;
removeConditionalBlock: () => ReturnType;
};
}
}
export const ConditionalBlock = Node.create<ConditionalBlockOptions>({
name: "conditionalBlock",
group: "block",
content: "block+",
defining: true,
isolating: true,
addOptions() {
return {
HTMLAttributes: {
class: "conditional-block"
}
};
},
addAttributes() {
return {
conditionType: {
default: "auth",
parseHTML: (element) => element.getAttribute("data-condition-type"),
renderHTML: (attributes) => ({
"data-condition-type": attributes.conditionType
})
},
conditionValue: {
default: "authenticated",
parseHTML: (element) => element.getAttribute("data-condition-value"),
renderHTML: (attributes) => ({
"data-condition-value": attributes.conditionValue
})
},
showWhen: {
default: "true",
parseHTML: (element) => element.getAttribute("data-show-when"),
renderHTML: (attributes) => ({
"data-show-when": attributes.showWhen
})
}
};
},
parseHTML() {
return [
{
tag: "div.conditional-block[data-condition-type]"
}
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
["div", { class: "conditional-content" }, 0]
];
},
addCommands() {
return {
setConditionalBlock:
(attributes) =>
({ commands }) => {
return commands.wrapIn(this.name, attributes);
},
updateConditionalBlock:
(attributes) =>
({ commands }) => {
return commands.updateAttributes(this.name, attributes);
},
removeConditionalBlock:
() =>
({ commands }) => {
return commands.lift(this.name);
}
};
}
});

View File

@@ -0,0 +1,101 @@
import { Mark, mergeAttributes } from "@tiptap/core";
export interface ConditionalInlineOptions {
HTMLAttributes: Record<string, any>;
}
export interface ConditionalInlineAttributes {
conditionType: "auth" | "privilege" | "date" | "feature" | "env";
conditionValue: string;
showWhen: "true" | "false";
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
conditionalInline: {
setConditionalInline: (
attributes: ConditionalInlineAttributes
) => ReturnType;
toggleConditionalInline: (
attributes: ConditionalInlineAttributes
) => ReturnType;
unsetConditionalInline: () => ReturnType;
};
}
}
export const ConditionalInline = Mark.create<ConditionalInlineOptions>({
name: "conditionalInline",
priority: 1000,
keepOnSplit: false,
addOptions() {
return {
HTMLAttributes: {
class: "conditional-inline"
}
};
},
addAttributes() {
return {
conditionType: {
default: "auth",
parseHTML: (element) => element.getAttribute("data-condition-type"),
renderHTML: (attributes) => ({
"data-condition-type": attributes.conditionType
})
},
conditionValue: {
default: "authenticated",
parseHTML: (element) => element.getAttribute("data-condition-value"),
renderHTML: (attributes) => ({
"data-condition-value": attributes.conditionValue
})
},
showWhen: {
default: "true",
parseHTML: (element) => element.getAttribute("data-show-when"),
renderHTML: (attributes) => ({
"data-show-when": attributes.showWhen
})
}
};
},
parseHTML() {
return [
{
tag: "span.conditional-inline[data-condition-type]"
}
];
},
renderHTML({ HTMLAttributes }) {
return [
"span",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0
];
},
addCommands() {
return {
setConditionalInline:
(attributes) =>
({ commands }) => {
return commands.setMark(this.name, attributes);
},
toggleConditionalInline:
(attributes) =>
({ commands }) => {
return commands.toggleMark(this.name, attributes);
},
unsetConditionalInline:
() =>
({ commands }) => {
return commands.unsetMark(this.name);
}
};
}
});

View File

@@ -12,6 +12,11 @@ export default createHandler(() => (
{assets}
</head>
<body>
<noscript>
<div style="position: fixed; top: 0; left: 0; right: 0; z-index: 9999; background-color: var(--color-yellow); color: var(--color-crust); padding: 1rem; text-align: center; font-weight: 600; border-bottom: 2px solid var(--color-text);">
JavaScript is disabled. Features will be limited.
</div>
</noscript>
<div id="app">{children}</div>
{scripts}
</body>

View File

@@ -26,6 +26,9 @@ const getPostByTitle = query(
"use server";
const { ConnectionFactory, getUserID, getPrivilegeLevel } =
await import("~/server/utils");
const { parseConditionals, getSafeEnvVariables } =
await import("~/server/conditional-parser");
const { getFeatureFlags } = await import("~/server/feature-flags");
const event = getRequestEvent()!;
const privilegeLevel = await getPrivilegeLevel(event.nativeEvent);
const userID = await getUserID(event.nativeEvent);
@@ -78,6 +81,26 @@ const getPostByTitle = query(
};
}
// Build conditional evaluation context
const conditionalContext = {
isAuthenticated: userID !== null,
privilegeLevel: privilegeLevel,
userId: userID,
currentDate: new Date(),
featureFlags: getFeatureFlags(),
env: getSafeEnvVariables()
};
// Parse conditionals in post body
if (post.body) {
try {
post.body = parseConditionals(post.body, conditionalContext);
} catch (error) {
console.error("Error parsing conditionals in post body:", error);
// Fall back to showing original content
}
}
// Fetch comments with sorting
let commentQuery = "SELECT * FROM Comment WHERE post_id = ?";
@@ -274,10 +297,10 @@ export default function PostPage() {
</div>
{/* Spacer to push content down */}
<div class="h-80 sm:h-96 md:h-[50vh]"></div>
<div class="-mt-[10vh] h-80 sm:h-96 md:h-[50vh]" />
{/* Content that slides over the fixed image */}
<div class="bg-surface0 relative z-40 pb-24">
<div class="bg-base relative z-40 pb-24">
<div class="top-4 flex w-full flex-col justify-center md:absolute md:flex-row md:justify-between">
<div class="">
<div class="flex justify-center italic md:justify-start md:pl-24">
@@ -287,7 +310,7 @@ export default function PostPage() {
By Michael Freno
</div>
</div>
<div class="flex max-w-[420px] flex-wrap justify-center italic md:justify-start md:pl-24">
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
<For each={postData.tags as any[]}>
{(tag) => (
<div class="group relative m-1 h-fit w-fit rounded-xl bg-purple-600 px-2 py-1 text-sm">

View File

@@ -336,14 +336,14 @@ export default function LoginPage() {
<div class="relative pt-12 md:pt-24">
{/* Error message */}
<Show when={error()}>
<div class="mb-4 w-full max-w-md rounded-lg border border-red-500 bg-red-500/10 px-4 py-3 text-center">
<div class="border-maroon bg-red mb-4 w-full max-w-md rounded-lg border px-4 py-3 text-center">
<Show when={error() === "passwordMismatch"}>
<div class="text-lg font-semibold text-red-500">
<div class="text-red text-lg font-semibold">
Passwords did not match!
</div>
</Show>
<Show when={error() === "duplicate"}>
<div class="text-lg font-semibold text-red-500">
<div class="text-red text-lg font-semibold">
Email Already Exists!
</div>
</Show>
@@ -354,7 +354,7 @@ export default function LoginPage() {
error() !== "duplicate"
}
>
<div class="text-sm text-red-500">{error()}</div>
<div class="text-red text-sm">{error()}</div>
</Show>
</div>
</Show>
@@ -519,7 +519,7 @@ export default function LoginPage() {
passwordConfRef.value.length >= 6
? ""
: "opacity-0 select-none"
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
} text-red text-center transition-opacity duration-200 ease-in-out`}
>
Passwords do not match!
</div>
@@ -535,11 +535,11 @@ export default function LoginPage() {
<div
class={`${
showPasswordError()
? "text-red-500"
? "text-red"
: showPasswordSuccess()
? "text-green-500"
? "text-green"
: "opacity-0 select-none"
} flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
} flex min-h-4 justify-center italic transition-opacity duration-300 ease-in-out`}
>
<Show when={showPasswordError()}>
Credentials did not match any record
@@ -576,7 +576,7 @@ export default function LoginPage() {
initialRemainingTime={countDown()}
size={48}
strokeWidth={6}
colors="#60a5fa"
colors="var(--color-blue)"
>
{renderTime}
</CountdownCircleTimer>

View File

@@ -0,0 +1,288 @@
import { describe, it, expect } from "bun:test";
import {
parseConditionals,
type ConditionalContext
} from "./conditional-parser";
describe("parseConditionals", () => {
const baseContext: ConditionalContext = {
isAuthenticated: true,
privilegeLevel: "user",
userId: "test-user",
currentDate: new Date("2025-06-01"),
featureFlags: { "beta-feature": true },
env: { NODE_ENV: "development", VERCEL_ENV: "development" }
};
it("should show content for authenticated users", () => {
const html = `
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
<div class="conditional-content"><p>Secret content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Secret content");
expect(result).not.toContain("conditional-block");
});
it("should hide content for anonymous users when condition is authenticated", () => {
const html = `
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
<div class="conditional-content"><p>Secret content</p></div>
</div>
`;
const anonContext: ConditionalContext = {
...baseContext,
isAuthenticated: false,
privilegeLevel: "anonymous"
};
const result = parseConditionals(html, anonContext);
expect(result).not.toContain("Secret content");
});
it("should evaluate admin-only content", () => {
const html = `
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
<div class="conditional-content"><p>Admin panel</p></div>
</div>
`;
const userResult = parseConditionals(html, baseContext);
expect(userResult).not.toContain("Admin panel");
const adminContext: ConditionalContext = {
...baseContext,
privilegeLevel: "admin"
};
const adminResult = parseConditionals(html, adminContext);
expect(adminResult).toContain("Admin panel");
});
it("should handle date before condition", () => {
const html = `
<div class="conditional-block" data-condition-type="date" data-condition-value="before:2026-01-01" data-show-when="true">
<div class="conditional-content"><p>Available until 2026</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Available until 2026");
});
it("should handle date after condition", () => {
const html = `
<div class="conditional-block" data-condition-type="date" data-condition-value="after:2024-01-01" data-show-when="true">
<div class="conditional-content"><p>Available after 2024</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Available after 2024");
});
it("should handle date between condition", () => {
const html = `
<div class="conditional-block" data-condition-type="date" data-condition-value="between:2025-01-01,2025-12-31" data-show-when="true">
<div class="conditional-content"><p>2025 content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("2025 content");
});
it("should handle feature flag conditions", () => {
const html = `
<div class="conditional-block" data-condition-type="feature" data-condition-value="beta-feature" data-show-when="true">
<div class="conditional-content"><p>Beta content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Beta content");
});
it("should hide content when feature flag is false", () => {
const html = `
<div class="conditional-block" data-condition-type="feature" data-condition-value="disabled-feature" data-show-when="true">
<div class="conditional-content"><p>Disabled content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Disabled content");
});
it("should handle showWhen=false (inverted logic)", () => {
const html = `
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="false">
<div class="conditional-content"><p>Not authenticated content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Not authenticated content");
const anonContext: ConditionalContext = {
...baseContext,
isAuthenticated: false,
privilegeLevel: "anonymous"
};
const anonResult = parseConditionals(html, anonContext);
expect(anonResult).toContain("Not authenticated content");
});
it("should handle multiple conditional blocks", () => {
const html = `
<p>Public content</p>
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
<div class="conditional-content"><p>Auth content</p></div>
</div>
<p>More public</p>
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
<div class="conditional-content"><p>Admin content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Public content");
expect(result).toContain("Auth content");
expect(result).toContain("More public");
expect(result).not.toContain("Admin content");
});
it("should handle empty HTML", () => {
const result = parseConditionals("", baseContext);
expect(result).toBe("");
});
it("should handle HTML with no conditionals", () => {
const html = "<p>Regular content</p>";
const result = parseConditionals(html, baseContext);
expect(result).toBe(html);
});
it("should default to hiding unknown condition types", () => {
const html = `
<div class="conditional-block" data-condition-type="unknown" data-condition-value="something" data-show-when="true">
<div class="conditional-content"><p>Unknown type content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Unknown type content");
});
it("should handle complex nested HTML in conditional content", () => {
const html = `
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
<div class="conditional-content">
<h2>Title</h2>
<ul><li>Item 1</li><li>Item 2</li></ul>
<pre><code>console.log('test');</code></pre>
</div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("<h2>Title</h2>");
expect(result).toContain("<ul><li>Item 1</li>");
expect(result).toContain("<code>console.log('test');</code>");
});
it("should handle env condition with exact match", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">
<div class="conditional-content"><p>Dev mode content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Dev mode content");
});
it("should hide env condition when value doesn't match", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="true">
<div class="conditional-content"><p>Prod content</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Prod content");
});
it("should handle env condition with wildcard (*) for any truthy value", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:*" data-show-when="true">
<div class="conditional-content"><p>Any env set</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Any env set");
});
it("should hide env condition when variable is undefined", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="NONEXISTENT_VAR:*" data-show-when="true">
<div class="conditional-content"><p>Should not show</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Should not show");
});
it("should handle env condition with inverted logic", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="false">
<div class="conditional-content"><p>Not production</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Not production");
});
it("should handle malformed env condition format", () => {
const html = `
<div class="conditional-block" data-condition-type="env" data-condition-value="INVALID_FORMAT" data-show-when="true">
<div class="conditional-content"><p>Invalid format</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).not.toContain("Invalid format");
});
// Inline conditional tests
it("should handle inline conditional span for authenticated users", () => {
const html = `<p>The domain is <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">localhost</span>.</p>`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("The domain is localhost.");
expect(result).not.toContain("conditional-inline");
expect(result).not.toContain("data-condition-type");
});
it("should hide inline conditional when condition is false", () => {
const html = `<p>The domain is <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="true">freno.me</span>.</p>`;
const result = parseConditionals(html, baseContext);
expect(result).toBe("<p>The domain is .</p>");
});
it("should handle inline auth conditionals", () => {
const html = `<p>Welcome <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">back</span>!</p>`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Welcome back!");
});
it("should handle multiple inline conditionals in same paragraph", () => {
const html = `<p>Domain: <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">localhost</span>, User: <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">logged in</span></p>`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Domain: localhost");
expect(result).toContain("User: logged in");
});
it("should handle mixed block and inline conditionals", () => {
const html = `
<p>Text with <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">inline</span> conditional.</p>
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
<div class="conditional-content"><p>Block conditional</p></div>
</div>
`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Text with inline conditional.");
expect(result).not.toContain("Block conditional"); // user is not admin
});
it("should handle inline conditional with showWhen=false", () => {
const html = `<p>Status: <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="false">not production</span></p>`;
const result = parseConditionals(html, baseContext);
expect(result).toContain("Status: not production");
});
});

View File

@@ -0,0 +1,309 @@
/**
* Server-side conditional parser for blog content
* Evaluates conditional blocks and returns processed HTML
*/
/**
* Get safe environment variables for conditional evaluation
* Only exposes non-sensitive variables that are safe to use in content conditionals
*/
export function getSafeEnvVariables(): Record<string, string | undefined> {
return {
NODE_ENV: process.env.NODE_ENV,
VERCEL_ENV: process.env.VERCEL_ENV
// Add other safe, non-sensitive env vars here as needed
// DO NOT expose API keys, secrets, database URLs, etc.
};
}
export interface ConditionalContext {
isAuthenticated: boolean;
privilegeLevel: "admin" | "user" | "anonymous";
userId: string | null;
currentDate: Date;
featureFlags: Record<string, boolean>;
env: Record<string, string | undefined>;
}
interface ConditionalBlock {
fullMatch: string;
conditionType: string;
conditionValue: string;
showWhen: string;
content: string;
}
/**
* Parse HTML and evaluate conditional blocks (both block and inline)
* @param html - Raw HTML from database
* @param context - Evaluation context (user, date, features)
* @returns Processed HTML with conditionals evaluated
*/
export function parseConditionals(
html: string,
context: ConditionalContext
): string {
if (!html) return html;
let processedHtml = html;
// First, process block-level conditionals (div elements)
processedHtml = processBlockConditionals(processedHtml, context);
// Then, process inline conditionals (span elements)
processedHtml = processInlineConditionals(processedHtml, context);
return processedHtml;
}
/**
* Process block-level conditional divs
*/
function processBlockConditionals(
html: string,
context: ConditionalContext
): string {
// Regex to match conditional blocks
// Matches: <div class="conditional-block" data-condition-type="..." data-condition-value="..." data-show-when="...">...</div>
const conditionalRegex =
/<div\s+[^>]*class="[^"]*conditional-block[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/div>/gi;
let processedHtml = html;
let match: RegExpExecArray | null;
// Reset regex lastIndex
conditionalRegex.lastIndex = 0;
// Collect all matches first to avoid regex state issues
const matches: ConditionalBlock[] = [];
while ((match = conditionalRegex.exec(html)) !== null) {
matches.push({
fullMatch: match[0],
conditionType: match[1],
conditionValue: match[2],
showWhen: match[3],
content: match[4]
});
}
// Process each conditional block
for (const block of matches) {
const shouldShow = evaluateCondition(
block.conditionType,
block.conditionValue,
block.showWhen === "true",
context
);
if (shouldShow) {
// Keep content, but remove conditional wrapper
// Extract content from inner <div class="conditional-content">
const innerContentRegex =
/<div\s+class="conditional-content">([\s\S]*?)<\/div>/i;
const innerMatch = block.fullMatch.match(innerContentRegex);
const innerContent = innerMatch ? innerMatch[1] : block.content;
processedHtml = processedHtml.replace(block.fullMatch, innerContent);
} else {
// Remove entire block
processedHtml = processedHtml.replace(block.fullMatch, "");
}
}
return processedHtml;
}
/**
* Process inline conditional spans
*/
function processInlineConditionals(
html: string,
context: ConditionalContext
): string {
// Regex to match inline conditionals
// Matches: <span class="conditional-inline" data-condition-type="..." data-condition-value="..." data-show-when="...">...</span>
const inlineRegex =
/<span\s+[^>]*class="[^"]*conditional-inline[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/span>/gi;
let processedHtml = html;
let match: RegExpExecArray | null;
// Reset regex lastIndex
inlineRegex.lastIndex = 0;
// Collect all matches first
const matches: ConditionalBlock[] = [];
while ((match = inlineRegex.exec(html)) !== null) {
matches.push({
fullMatch: match[0],
conditionType: match[1],
conditionValue: match[2],
showWhen: match[3],
content: match[4]
});
}
// Process each inline conditional
for (const inline of matches) {
const shouldShow = evaluateCondition(
inline.conditionType,
inline.conditionValue,
inline.showWhen === "true",
context
);
if (shouldShow) {
// Keep content, remove span wrapper
processedHtml = processedHtml.replace(inline.fullMatch, inline.content);
} else {
// Remove entire inline span
processedHtml = processedHtml.replace(inline.fullMatch, "");
}
}
return processedHtml;
}
/**
* Evaluate a single condition
*/
function evaluateCondition(
conditionType: string,
conditionValue: string,
showWhen: boolean,
context: ConditionalContext
): boolean {
let conditionMet = false;
switch (conditionType) {
case "auth":
conditionMet = evaluateAuthCondition(conditionValue, context);
break;
case "privilege":
conditionMet = evaluatePrivilegeCondition(conditionValue, context);
break;
case "date":
conditionMet = evaluateDateCondition(conditionValue, context);
break;
case "feature":
conditionMet = evaluateFeatureCondition(conditionValue, context);
break;
case "env":
conditionMet = evaluateEnvCondition(conditionValue, context);
break;
default:
// Unknown condition type - default to hiding content for safety
conditionMet = false;
}
// Apply showWhen logic: if showWhen is true, show when condition is met
// If showWhen is false, show when condition is NOT met
return showWhen ? conditionMet : !conditionMet;
}
/**
* Evaluate authentication condition
*/
function evaluateAuthCondition(
value: string,
context: ConditionalContext
): boolean {
switch (value) {
case "authenticated":
return context.isAuthenticated;
case "anonymous":
return !context.isAuthenticated;
default:
return false;
}
}
/**
* Evaluate privilege level condition
*/
function evaluatePrivilegeCondition(
value: string,
context: ConditionalContext
): boolean {
return context.privilegeLevel === value;
}
/**
* Evaluate date-based condition
* Supports: "before:YYYY-MM-DD", "after:YYYY-MM-DD", "between:YYYY-MM-DD,YYYY-MM-DD"
*/
function evaluateDateCondition(
value: string,
context: ConditionalContext
): boolean {
try {
const now = context.currentDate.getTime();
if (value.startsWith("before:")) {
const dateStr = value.substring(7);
const targetDate = new Date(dateStr).getTime();
return now < targetDate;
}
if (value.startsWith("after:")) {
const dateStr = value.substring(6);
const targetDate = new Date(dateStr).getTime();
return now > targetDate;
}
if (value.startsWith("between:")) {
const dateRange = value.substring(8).split(",");
if (dateRange.length !== 2) return false;
const startDate = new Date(dateRange[0].trim()).getTime();
const endDate = new Date(dateRange[1].trim()).getTime();
return now >= startDate && now <= endDate;
}
return false;
} catch (error) {
console.error("Error parsing date condition:", error);
return false;
}
}
/**
* Evaluate feature flag condition
*/
function evaluateFeatureCondition(
value: string,
context: ConditionalContext
): boolean {
return context.featureFlags[value] === true;
}
/**
* Evaluate environment variable condition
* Format: "ENV_VAR_NAME:expected_value" or "ENV_VAR_NAME:*" for any truthy value
*/
function evaluateEnvCondition(
value: string,
context: ConditionalContext
): boolean {
try {
// Parse format: "VAR_NAME:expected_value"
const colonIndex = value.indexOf(":");
if (colonIndex === -1) return false;
const varName = value.substring(0, colonIndex).trim();
const expectedValue = value.substring(colonIndex + 1).trim();
const actualValue = context.env[varName];
// If expected value is "*", check if variable exists and is truthy
if (expectedValue === "*") {
return !!actualValue;
}
// Otherwise, check for exact match
return actualValue === expectedValue;
} catch (error) {
console.error("Error parsing env condition:", error);
return false;
}
}

View File

@@ -0,0 +1,24 @@
/**
* Feature flag system for conditional content
* Centralized configuration for feature toggles
*/
export interface FeatureFlags {
[key: string]: boolean;
}
export function getFeatureFlags(): FeatureFlags {
return {
// TODO: Add feature flags here
"beta-features": process.env.ENABLE_BETA_FEATURES === "true",
"new-editor": false,
"premium-content": true,
"seasonal-event": false,
"maintenance-mode": false
};
}
export function isFeatureEnabled(featureName: string): boolean {
const flags = getFeatureFlags();
return flags[featureName] === true;
}