diff --git a/src/app.css b/src/app.css
index e6f1f35..5b39aab 100644
--- a/src/app.css
+++ b/src/app.css
@@ -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: "🔒❌";
+}
diff --git a/src/app.tsx b/src/app.tsx
index 7b9ecb5..4bf476c 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -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 }) {
+
{resolved()}
diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx
index 523b3a6..b7f2e27 100644
--- a/src/components/blog/TextEditor.tsx
+++ b/src/components/blog/TextEditor.tsx
@@ -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 (
+
+
Conditional Block
+
+ {/* Condition Type Selector */}
+
+
+
+ {/* Dynamic Condition Value Input based on type */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setConditionalForm({
+ ...conditionalForm(),
+ conditionValue: e.currentTarget.value
+ })
+ }
+ />
+
+ Format: before:YYYY-MM-DD, after:YYYY-MM-DD, or
+ between:YYYY-MM-DD,YYYY-MM-DD
+
+
+
+
+
+
+ setConditionalForm({
+ ...conditionalForm(),
+ conditionValue: e.currentTarget.value
+ })
+ }
+ />
+
+
+
+
+
+ setConditionalForm({
+ ...conditionalForm(),
+ conditionValue: e.currentTarget.value
+ })
+ }
+ />
+
+
+ Format: VAR_NAME:value or VAR_NAME:* for any truthy value
+
+
+
+ {/* Show When Toggle */}
+
+
+
+ {/* Inline Toggle */}
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+ );
+ };
+
return (
+ {/* Conditional Configurator */}
+
+
+
+
+
+
{/* Main Toolbar - Pinned at top in fullscreen */}
📊 Diagram
+
{/* Spacer to push content down */}
-
+
{/* Content that slides over the fixed image */}
-
+
@@ -287,7 +310,7 @@ export default function PostPage() {
By Michael Freno
-
+
{(tag) => (
diff --git a/src/routes/login/index.tsx b/src/routes/login/index.tsx
index b0d4ffc..80af48a 100644
--- a/src/routes/login/index.tsx
+++ b/src/routes/login/index.tsx
@@ -336,14 +336,14 @@ export default function LoginPage() {
{/* Error message */}
-
+
-
+
Passwords did not match!
-
+
Email Already Exists!
@@ -354,7 +354,7 @@ export default function LoginPage() {
error() !== "duplicate"
}
>
-
{error()}
+
{error()}
@@ -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!
@@ -535,11 +535,11 @@ export default function LoginPage() {
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}
diff --git a/src/server/conditional-parser.test.ts b/src/server/conditional-parser.test.ts
new file mode 100644
index 0000000..9f6314b
--- /dev/null
+++ b/src/server/conditional-parser.test.ts
@@ -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 = `
+
+ `;
+ 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 = `
+
+ `;
+ 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 = `
+
+ `;
+ 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 = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Available until 2026");
+ });
+
+ it("should handle date after condition", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Available after 2024");
+ });
+
+ it("should handle date between condition", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("2025 content");
+ });
+
+ it("should handle feature flag conditions", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Beta content");
+ });
+
+ it("should hide content when feature flag is false", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).not.toContain("Disabled content");
+ });
+
+ it("should handle showWhen=false (inverted logic)", () => {
+ const html = `
+
+
Not authenticated content
+
+ `;
+ 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 = `
+ Public content
+
+ More public
+
+ `;
+ 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 = "Regular content
";
+ const result = parseConditionals(html, baseContext);
+ expect(result).toBe(html);
+ });
+
+ it("should default to hiding unknown condition types", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).not.toContain("Unknown type content");
+ });
+
+ it("should handle complex nested HTML in conditional content", () => {
+ const html = `
+
+
+
Title
+
+
console.log('test');
+
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Title
");
+ expect(result).toContain("- Item 1
");
+ expect(result).toContain("console.log('test');");
+ });
+
+ it("should handle env condition with exact match", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Dev mode content");
+ });
+
+ it("should hide env condition when value doesn't match", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).not.toContain("Prod content");
+ });
+
+ it("should handle env condition with wildcard (*) for any truthy value", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Any env set");
+ });
+
+ it("should hide env condition when variable is undefined", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).not.toContain("Should not show");
+ });
+
+ it("should handle env condition with inverted logic", () => {
+ const html = `
+
+ `;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Not production");
+ });
+
+ it("should handle malformed env condition format", () => {
+ const html = `
+
+ `;
+ 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 = `The domain is localhost.
`;
+ 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 = `The domain is freno.me.
`;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toBe("The domain is .
");
+ });
+
+ it("should handle inline auth conditionals", () => {
+ const html = `Welcome back!
`;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Welcome back!");
+ });
+
+ it("should handle multiple inline conditionals in same paragraph", () => {
+ const html = `Domain: localhost, User: logged in
`;
+ 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 = `
+ Text with inline conditional.
+
+ `;
+ 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 = `Status: not production
`;
+ const result = parseConditionals(html, baseContext);
+ expect(result).toContain("Status: not production");
+ });
+});
diff --git a/src/server/conditional-parser.ts b/src/server/conditional-parser.ts
new file mode 100644
index 0000000..110cf01
--- /dev/null
+++ b/src/server/conditional-parser.ts
@@ -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 {
+ 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;
+ env: Record;
+}
+
+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: ...
+ const conditionalRegex =
+ /]*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
+ const innerContentRegex =
+ /
([\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: ...
+ const inlineRegex =
+ /]*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;
+ }
+}
diff --git a/src/server/feature-flags.ts b/src/server/feature-flags.ts
new file mode 100644
index 0000000..81941c0
--- /dev/null
+++ b/src/server/feature-flags.ts
@@ -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;
+}