diff --git a/frontend/components/Notes.tsx b/frontend/components/Notes.tsx
index 7ad2bc3..f9874d4 100644
--- a/frontend/components/Notes.tsx
+++ b/frontend/components/Notes.tsx
@@ -1,27 +1,93 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
+import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/solid';
+import {
+ PencilIcon,
+ TrashIcon,
+ FolderIcon,
+ TagIcon as TagIconOutline,
+ FunnelIcon,
+ ClockIcon,
+ EllipsisVerticalIcon,
+ XMarkIcon,
+} from '@heroicons/react/24/outline';
import { useToast } from './Shared/ToastContext';
-import SortFilterButton, { SortOption } from './Shared/SortFilterButton';
+import { Link, useParams, useNavigate } from 'react-router-dom';
+import { SortOption } from './Shared/SortFilterButton';
import NoteModal from './Note/NoteModal';
import ConfirmDialog from './Shared/ConfirmDialog';
-import NoteCard from './Shared/NoteCard';
+import DiscardChangesDialog from './Shared/DiscardChangesDialog';
+import MarkdownRenderer from './Shared/MarkdownRenderer';
+import TagInput from './Tag/TagInput';
import { Note } from '../entities/Note';
import { createNote, updateNote } from '../utils/notesService';
import { deleteNoteWithStoreUpdate } from '../utils/noteDeleteUtils';
import { useStore } from '../store/useStore';
import { createProject } from '../utils/projectsService';
+import { ENABLE_NOTE_COLOR } from '../config/featureFlags';
+
+// Define colors for note backgrounds
+const NOTE_COLORS = [
+ { name: 'None', value: '' },
+ { name: 'Red', value: '#B71C1C' },
+ { name: 'Orange', value: '#E65100' },
+ { name: 'Amber', value: '#FF8F00' },
+ { name: 'Green', value: '#2E7D32' },
+ { name: 'Teal', value: '#00695C' },
+ { name: 'Blue', value: '#1565C0' },
+ { name: 'Indigo', value: '#283593' },
+ { name: 'Purple', value: '#6A1B9A' },
+ { name: 'Pink', value: '#AD1457' },
+ { name: 'Grey', value: '#424242' },
+];
+
+// Helper function to determine if we should use light text on a dark background
+const shouldUseLightText = (hexColor: string | undefined): boolean => {
+ if (!hexColor) return false;
+
+ // Convert hex to RGB
+ const hex = hexColor.replace('#', '');
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+
+ // Calculate relative luminance
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+
+ // Use light text if luminance is below 0.4
+ return luminance < 0.4;
+};
const Notes: React.FC = () => {
const { t } = useTranslation();
const { showSuccessToast } = useToast();
+ const { uid } = useParams<{ uid?: string }>();
+ const navigate = useNavigate();
const [selectedNote, setSelectedNote] = useState
(null);
+ const [previewNote, setPreviewNote] = useState(null);
+ const [isEditing, setIsEditing] = useState(false);
+ const [editingNote, setEditingNote] = useState(null);
const [isNoteModalOpen, setIsNoteModalOpen] = useState(false);
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
const [noteToDelete, setNoteToDelete] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
- const [isSearchExpanded, setIsSearchExpanded] = useState(false);
const [orderBy, setOrderBy] = useState('created_at:desc');
+ const [showProjectDropdown, setShowProjectDropdown] = useState(false);
+ const [showTagsInput, setShowTagsInput] = useState(false);
+ const [showDiscardDialog, setShowDiscardDialog] = useState(false);
+ const [showSortDropdown, setShowSortDropdown] = useState(false);
+ const [showNoteOptionsDropdown, setShowNoteOptionsDropdown] =
+ useState(false);
+ const hasAutoSelected = useRef(false);
+
+ const editingNoteColor =
+ ENABLE_NOTE_COLOR && editingNote ? editingNote.color : undefined;
+ const previewNoteColor =
+ ENABLE_NOTE_COLOR && previewNote ? previewNote.color : undefined;
+ const activeNoteColor =
+ (isEditing && editingNoteColor) || previewNoteColor || undefined;
+ const sortDropdownRef = useRef(null);
+ const noteOptionsDropdownRef = useRef(null);
// Get notes and projects from global store
const { notes, isLoading, isError, hasLoaded, loadNotes, setNotes } =
@@ -48,20 +114,158 @@ const Notes: React.FC = () => {
setOrderBy(newOrderBy);
};
+ // Function to set preview note and update URL
+ const handleSelectNote = (note: Note | null) => {
+ // If we're editing a new unsaved note, discard it
+ if (isEditing && editingNote && !editingNote.uid) {
+ setIsEditing(false);
+ setEditingNote(null);
+ setShowProjectDropdown(false);
+ setShowTagsInput(false);
+ }
+
+ setPreviewNote(note);
+ if (note?.uid) {
+ navigate(`/notes/${note.uid}`, { replace: true });
+ } else {
+ navigate('/notes', { replace: true });
+ }
+ };
+
const handleDeleteNote = async () => {
if (!noteToDelete) return;
try {
await deleteNoteWithStoreUpdate(noteToDelete, showSuccessToast, t);
setIsConfirmDialogOpen(false);
setNoteToDelete(null);
+ // If we deleted the currently viewed note, clear the URL
+ if (previewNote?.uid === noteToDelete.uid) {
+ navigate('/notes', { replace: true });
+ }
} catch (err) {
console.error('Error deleting note:', err);
}
};
const handleEditNote = (note: Note) => {
- setSelectedNote(note);
- setIsNoteModalOpen(true);
+ // Handle both lowercase and uppercase versions from Sequelize
+ const project = note.project || note.Project;
+ const tags = note.tags || note.Tags || [];
+
+ setEditingNote({
+ ...note,
+ project_uid: project?.uid || note.project_uid,
+ project: project,
+ tags: tags,
+ });
+ setIsEditing(true);
+ handleSelectNote(null);
+ };
+
+ const handleNewNote = () => {
+ setEditingNote({
+ title: '',
+ content: '',
+ tags: [],
+ });
+ setIsEditing(true);
+ handleSelectNote(null);
+ };
+
+ const handleSaveInlineNote = async () => {
+ if (!editingNote || !editingNote.title) return;
+
+ try {
+ // Add new tags to store if they don't exist
+ if (editingNote.tags && editingNote.tags.length > 0) {
+ const { tagsStore } = useStore.getState();
+ tagsStore.addNewTags(editingNote.tags.map((t) => t.name));
+ }
+
+ if (editingNote.uid) {
+ const savedNote = await updateNote(
+ editingNote.uid,
+ editingNote
+ );
+ const updatedNotes = notes.map((note) =>
+ note.uid === editingNote.uid ? savedNote : note
+ );
+ setNotes(updatedNotes);
+ handleSelectNote(savedNote);
+ } else {
+ const newNote = await createNote(editingNote);
+ setNotes([newNote, ...notes]);
+ handleSelectNote(newNote);
+ }
+ setIsEditing(false);
+ setEditingNote(null);
+ setShowProjectDropdown(false);
+ setShowTagsInput(false);
+ } catch (err) {
+ console.error('Error saving note:', err);
+ }
+ };
+
+ const handleCancelEdit = () => {
+ setIsEditing(false);
+ setEditingNote(null);
+ setShowProjectDropdown(false);
+ setShowTagsInput(false);
+ if (previewNote) {
+ // If we were editing an existing note, go back to preview
+ handleSelectNote(previewNote);
+ }
+ };
+
+ const handleProjectButtonClick = (
+ e: React.MouseEvent
+ ) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setShowProjectDropdown((prev) => !prev);
+ setShowTagsInput(false);
+ };
+
+ const handleTagsButtonClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setShowTagsInput((prev) => !prev);
+ setShowProjectDropdown(false);
+ };
+
+ const handleColorChange = async (color: string, note: Note) => {
+ if (!ENABLE_NOTE_COLOR) return;
+ try {
+ const updatedNote = { ...note, color };
+
+ // Update local state immediately for instant feedback
+ if (previewNote?.uid === note.uid || !note.uid) {
+ setPreviewNote(updatedNote);
+ }
+ if (editingNote?.uid === note.uid || (isEditing && !note.uid)) {
+ setEditingNote(updatedNote);
+ }
+
+ // If note is saved, persist to backend
+ if (note.uid) {
+ const savedNote = await updateNote(note.uid, updatedNote);
+ const updatedNotes = notes.map((n) =>
+ n.uid === note.uid ? savedNote : n
+ );
+ setNotes(updatedNotes);
+
+ // Update states with backend response
+ if (previewNote?.uid === note.uid) {
+ setPreviewNote(savedNote);
+ }
+ if (editingNote?.uid === note.uid) {
+ setEditingNote(savedNote);
+ }
+ }
+ setShowNoteOptionsDropdown(false);
+ } catch (err) {
+ console.error('Error updating note color:', err);
+ }
};
const handleSaveNote = async (noteData: Note) => {
@@ -96,39 +300,124 @@ const Notes: React.FC = () => {
}
};
- const filteredNotes = notes.filter(
- (note) =>
- note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- note.content.toLowerCase().includes(searchQuery.toLowerCase())
- );
+ const filteredNotes = useMemo(() => {
+ return notes.filter(
+ (note) =>
+ note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ note.content.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }, [notes, searchQuery]);
// Sort the filtered notes
- const sortedNotes = [...filteredNotes].sort((a, b) => {
- const [field, direction] = orderBy.split(':');
- const isAsc = direction === 'asc';
+ const sortedNotes = useMemo(() => {
+ return [...filteredNotes].sort((a, b) => {
+ const [field, direction] = orderBy.split(':');
+ const isAsc = direction === 'asc';
- let valueA, valueB;
+ let valueA: string | number;
+ let valueB: string | number;
- switch (field) {
- case 'title':
- valueA = a.title?.toLowerCase() || '';
- valueB = b.title?.toLowerCase() || '';
- break;
- case 'updated_at':
- valueA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
- valueB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
- break;
- case 'created_at':
- default:
- valueA = a.created_at ? new Date(a.created_at).getTime() : 0;
- valueB = b.created_at ? new Date(b.created_at).getTime() : 0;
- break;
+ switch (field) {
+ case 'title':
+ valueA = a.title?.toLowerCase() || '';
+ valueB = b.title?.toLowerCase() || '';
+ break;
+ case 'updated_at':
+ valueA = a.updated_at
+ ? new Date(a.updated_at).getTime()
+ : 0;
+ valueB = b.updated_at
+ ? new Date(b.updated_at).getTime()
+ : 0;
+ break;
+ case 'created_at':
+ default:
+ valueA = a.created_at
+ ? new Date(a.created_at).getTime()
+ : 0;
+ valueB = b.created_at
+ ? new Date(b.created_at).getTime()
+ : 0;
+ break;
+ }
+
+ if (valueA < valueB) return isAsc ? -1 : 1;
+ if (valueA > valueB) return isAsc ? 1 : -1;
+ return 0;
+ });
+ }, [filteredNotes, orderBy]);
+
+ // Load note from URL parameter
+ useEffect(() => {
+ if (uid && sortedNotes.length > 0 && !hasAutoSelected.current) {
+ const noteFromUrl = sortedNotes.find((note) => note.uid === uid);
+ if (noteFromUrl) {
+ setPreviewNote(noteFromUrl);
+ hasAutoSelected.current = true;
+ } else if (!previewNote) {
+ // If note not found and no note selected, select first note on desktop
+ const isDesktop = window.innerWidth >= 768;
+ if (isDesktop) {
+ handleSelectNote(sortedNotes[0]);
+ hasAutoSelected.current = true;
+ }
+ }
}
+ }, [uid, sortedNotes]);
- if (valueA < valueB) return isAsc ? -1 : 1;
- if (valueA > valueB) return isAsc ? 1 : -1;
- return 0;
- });
+ // Auto-select first note when notes are loaded (only on desktop)
+ useEffect(() => {
+ if (
+ !uid &&
+ sortedNotes.length > 0 &&
+ !previewNote &&
+ !hasAutoSelected.current
+ ) {
+ // Check if we're on desktop (md breakpoint is 768px)
+ const isDesktop = window.innerWidth >= 768;
+ if (isDesktop) {
+ handleSelectNote(sortedNotes[0]);
+ hasAutoSelected.current = true;
+ }
+ }
+ }, [sortedNotes, previewNote, uid]);
+
+ // Handle clicking outside sort dropdown to close it
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ sortDropdownRef.current &&
+ !sortDropdownRef.current.contains(event.target as Node)
+ ) {
+ setShowSortDropdown(false);
+ }
+ if (
+ noteOptionsDropdownRef.current &&
+ !noteOptionsDropdownRef.current.contains(event.target as Node)
+ ) {
+ setShowNoteOptionsDropdown(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () =>
+ document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ // Handle Escape key to save and close editor
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && isEditing) {
+ e.preventDefault();
+ if (editingNote?.title) {
+ handleSaveInlineNote();
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleEscape);
+ return () => document.removeEventListener('keydown', handleEscape);
+ }, [isEditing, editingNote]);
if (isLoading) {
return (
@@ -149,88 +438,781 @@ const Notes: React.FC = () => {
}
return (
-
-
- {/* Notes Header */}
-
-
{t('notes.title')}
-
+
+
+ {/* Two-Column Layout */}
+
+ {/* Left Column - Notes List */}
+
+ {/* Notes Column Header */}
+
+
+ {t('notes.title')}
+
+
+ {/* Sort Filter Dropdown */}
+
+
+ {showSortDropdown && (
+
+
+ Sort by
+
+
+ {sortOptions.map((option) => (
+
+ ))}
+
+
+ )}
+
+
+
+
- {/* Header with Search and Sort Controls */}
-
-
- {/* Search Toggle Button */}
-
-
+ {/* Search Bar inside notes list */}
+
+
+
+
+ setSearchQuery(e.target.value)
+ }
+ className="w-full bg-transparent border-none focus:ring-0 focus:outline-none dark:text-white text-sm"
+ />
+
+
-
- {/* Sort Filter Button */}
-
-
+
+ {sortedNotes.length === 0 ? (
+
+
+ {t('notes.noNotesFound')}
+
+
+ ) : (
+ sortedNotes.map((note, index) => (
+
handleSelectNote(note)}
+ className={`p-5 cursor-pointer transition-colors ${
+ previewNote?.uid === note.uid
+ ? 'bg-white dark:bg-gray-900 mb-1'
+ : index !==
+ sortedNotes.length - 1
+ ? 'border-b border-gray-200/50 dark:border-gray-700/50 hover:bg-gray-50 dark:hover:bg-gray-800'
+ : 'hover:bg-gray-50 dark:hover:bg-gray-800'
+ }`}
+ >
+
+ {note.title ||
+ t(
+ 'notes.untitled',
+ 'Untitled Note'
+ )}
+
+
+ {note.content.substring(0, 100)}
+ {note.content.length > 100
+ ? '...'
+ : ''}
+
+
+ {new Date(
+ note.updated_at ||
+ note.created_at ||
+ ''
+ ).toLocaleDateString()}
+
+
+ ))
+ )}
-
- {/* Collapsible Search Bar */}
-
-
-
-
setSearchQuery(e.target.value)}
- className="w-full bg-transparent border-none focus:ring-0 focus:outline-none dark:text-white"
- />
+ {/* Right Column - Preview or Editor */}
+
+ {isEditing && editingNote ? (
+
+ {/* Editor Header - matches preview structure */}
+
+
+ {/* Back button for mobile */}
+
+
+ setEditingNote({
+ ...editingNote,
+ title: e.target.value,
+ })
+ }
+ onClick={(e) => e.stopPropagation()}
+ placeholder="Note title..."
+ className="w-full text-xl md:text-2xl font-bold bg-transparent text-gray-900 dark:text-gray-100 border-none focus:outline-none focus:ring-0 p-0 mb-1"
+ style={{
+ color: editingNoteColor
+ ? shouldUseLightText(
+ editingNoteColor
+ )
+ ? '#ffffff'
+ : '#333333'
+ : undefined,
+ }}
+ autoFocus
+ />
+
+
+
+
+ {editingNote.updated_at
+ ? new Date(
+ editingNote.updated_at
+ ).toLocaleDateString()
+ : 'New'}
+
+
+
+
+
+
+
+
+ {showNoteOptionsDropdown && (
+
+ {ENABLE_NOTE_COLOR && (
+
+
+ Background Color
+
+
+ {NOTE_COLORS.map(
+ (
+ colorOption
+ ) => (
+
+ )
+ )}
+
+
+ )}
+
+
+ {editingNote.uid && (
+
+ )}
+
+
+ )}
+
+
+
+ {/* Project Dropdown */}
+ {showProjectDropdown && (
+
+
+
+
+ )}
+
+ {/* Tags Input */}
+ {showTagsInput && (
+
+
+ t.name)}
+ onTagsChange={(
+ tagNames: string[]
+ ) => {
+ setEditingNote({
+ ...editingNote,
+ tags: tagNames.map(
+ (name: string) => ({
+ name,
+ })
+ ),
+ });
+ }}
+ availableTags={useStore
+ .getState()
+ .tagsStore.getTags()}
+ />
+
+ )}
+
+ {/* Content Editor */}
+
+
+
+ ) : previewNote ? (
+
+ {/* Preview Header */}
+
+
+ {/* Back button for mobile */}
+
+
+ handleEditNote(previewNote)
+ }
+ className="text-xl md:text-2xl font-bold mb-1 cursor-pointer text-gray-900 dark:text-gray-100 transition-colors"
+ style={{
+ color: previewNoteColor
+ ? shouldUseLightText(
+ previewNoteColor
+ )
+ ? '#ffffff'
+ : '#333333'
+ : undefined,
+ }}
+ title="Click to edit"
+ >
+ {previewNote.title ||
+ t(
+ 'notes.untitled',
+ 'Untitled Note'
+ )}
+
+
+
+
+
+ {new Date(
+ previewNote.updated_at ||
+ previewNote.created_at ||
+ ''
+ ).toLocaleDateString()}
+
+
+ {(previewNote.project ||
+ previewNote.Project) && (
+
+
+
+ e.stopPropagation()
+ }
+ >
+ {
+ (
+ previewNote.project ||
+ previewNote.Project
+ )?.name
+ }
+
+
+ )}
+ {((previewNote.tags &&
+ previewNote.tags.length > 0) ||
+ (previewNote.Tags &&
+ previewNote.Tags.length >
+ 0)) && (
+
+
+
+ {(
+ previewNote.tags ||
+ previewNote.Tags ||
+ []
+ ).map((tag, idx) => (
+
+ {idx > 0 &&
+ ', '}
+
+ e.stopPropagation()
+ }
+ >
+ {tag.name}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+ {showNoteOptionsDropdown && (
+
+ {ENABLE_NOTE_COLOR && (
+
+
+ Background Color
+
+
+ {NOTE_COLORS.map(
+ (
+ colorOption
+ ) => (
+
+ )
+ )}
+
+
+ )}
+
+
+
+
+
+ )}
+
+
+
+ {/* Preview Content */}
+
handleEditNote(previewNote)}
+ className="text-sm md:text-base flex-1 overflow-y-auto cursor-pointer px-6 md:px-8 py-4 text-gray-900 dark:text-gray-100"
+ style={{
+ color: previewNoteColor
+ ? shouldUseLightText(
+ previewNoteColor
+ )
+ ? '#ffffff'
+ : '#333333'
+ : undefined,
+ }}
+ title="Click to edit"
+ >
+
+
+
+ ) : (
+
+ {t(
+ 'notes.selectNote',
+ 'Select a note to preview'
+ )}
+
+ )}
- {/* Notes Grid */}
- {sortedNotes.length === 0 ? (
-
- {t('notes.noNotesFound')}
-
- ) : (
-
- {sortedNotes.map((note) => (
- {
- setNoteToDelete(note);
- setIsConfirmDialogOpen(true);
- }}
- showActions={true}
- showProject={true}
- />
- ))}
-
- )}
-
{/* NoteModal */}
{isNoteModalOpen && (
{
onCancel={() => setIsConfirmDialogOpen(false)}
/>
)}
+
+ {/* DiscardChangesDialog */}
+ {showDiscardDialog && (
+ {
+ setShowDiscardDialog(false);
+ handleCancelEdit();
+ }}
+ onCancel={() => setShowDiscardDialog(false)}
+ />
+ )}
);
diff --git a/frontend/components/Shared/MarkdownRenderer.tsx b/frontend/components/Shared/MarkdownRenderer.tsx
index 0722217..740e318 100644
--- a/frontend/components/Shared/MarkdownRenderer.tsx
+++ b/frontend/components/Shared/MarkdownRenderer.tsx
@@ -9,6 +9,7 @@ interface MarkdownRendererProps {
className?: string;
summaryMode?: boolean;
onContentChange?: (newContent: string) => void;
+ noteColor?: string;
}
const MarkdownRenderer: React.FC
= ({
@@ -16,7 +17,22 @@ const MarkdownRenderer: React.FC = ({
className = '',
summaryMode = false,
onContentChange,
+ noteColor,
}) => {
+ // Determine text color based on background
+ const getTextColor = () => {
+ if (!noteColor) return undefined;
+
+ const hex = noteColor.replace('#', '');
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+
+ return luminance < 0.4 ? '#ffffff' : '#333333';
+ };
+
+ const textColor = getTextColor();
useEffect(() => {
// Configure highlight.js
hljs.configure({
@@ -92,72 +108,108 @@ const MarkdownRenderer: React.FC = ({
h1: ({ ...props }) =>
summaryMode ? (
) : (
),
h2: ({ ...props }) =>
summaryMode ? (
) : (
),
h3: ({ ...props }) =>
summaryMode ? (
) : (
),
h4: ({ ...props }) =>
summaryMode ? (
) : (
),
h5: ({ ...props }) =>
summaryMode ? (
) : (
),
h6: ({ ...props }) =>
summaryMode ? (
) : (
),
@@ -165,7 +217,10 @@ const MarkdownRenderer: React.FC = ({
// Customize paragraph styles
p: ({ ...props }) => (
),
@@ -173,17 +228,35 @@ const MarkdownRenderer: React.FC = ({
// Customize list styles
ul: ({ ...props }) => (
),
ol: ({ ...props }) => (
+ ),
+ li: ({ ...props }) => (
+
),
- li: ({ ...props }) => ,
// Customize link styles
a: ({ ...props }) => (
@@ -225,7 +298,7 @@ const MarkdownRenderer: React.FC = ({
}
return (
{children}
@@ -293,7 +366,10 @@ const MarkdownRenderer: React.FC = ({
// Customize strong/bold text
strong: ({ ...props }) => (
),
@@ -301,7 +377,10 @@ const MarkdownRenderer: React.FC = ({
// Customize italic text
em: ({ ...props }) => (
),
diff --git a/frontend/config/featureFlags.ts b/frontend/config/featureFlags.ts
new file mode 100644
index 0000000..0a3bd8b
--- /dev/null
+++ b/frontend/config/featureFlags.ts
@@ -0,0 +1,21 @@
+const parseBooleanFlag = (
+ value: string | undefined,
+ defaultValue: boolean
+): boolean => {
+ if (value === undefined) return defaultValue;
+ const normalized = value.toString().toLowerCase();
+ return !['false', '0', 'off', 'no'].includes(normalized);
+};
+
+export const ENABLE_NOTE_COLOR = parseBooleanFlag(
+ process.env.ENABLE_NOTE_COLOR,
+ false
+);
+
+export type FeatureFlags = {
+ ENABLE_NOTE_COLOR: boolean;
+};
+
+export const featureFlags: FeatureFlags = {
+ ENABLE_NOTE_COLOR,
+};
diff --git a/frontend/entities/Note.ts b/frontend/entities/Note.ts
index c1d7432..1760f5f 100644
--- a/frontend/entities/Note.ts
+++ b/frontend/entities/Note.ts
@@ -9,6 +9,7 @@ export interface Note {
updated_at?: string;
project_id?: number; // Foreign key for project (deprecated, use project_uid)
project_uid?: string; // Foreign key for project by uid
+ color?: string; // Background color for the note
tags?: Tag[];
Tags?: Tag[]; // Sequelize association naming (capitalized)
project?: {
diff --git a/frontend/index.tsx b/frontend/index.tsx
index 5f54851..e7c01cd 100644
--- a/frontend/index.tsx
+++ b/frontend/index.tsx
@@ -1,10 +1,3 @@
-// Add type declaration for module.hot
-declare const module: {
- hot?: {
- accept: (path: string, callback: () => void) => void;
- };
-};
-
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
@@ -16,6 +9,29 @@ import './styles/markdown.css'; // Import markdown styles
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n'; // Import the i18n instance with its configuration
+const isDevelopment = process.env.NODE_ENV !== 'production';
+
+// Clear out any lingering service workers/caches from other branches (e.g. PWA)
+if (isDevelopment && 'serviceWorker' in navigator) {
+ navigator.serviceWorker.getRegistrations().then((registrations) => {
+ registrations.forEach((registration) => {
+ registration.unregister().catch(() => {
+ // Non-fatal during development cleanup
+ });
+ });
+ });
+
+ if ('caches' in window) {
+ caches.keys().then((cacheNames) => {
+ cacheNames.forEach((cacheName) => {
+ caches.delete(cacheName).catch(() => {
+ // Ignore cache cleanup failures during dev
+ });
+ });
+ });
+ }
+}
+
const storedPreference = localStorage.getItem('isDarkMode');
const prefersDarkMode = window.matchMedia(
'(prefers-color-scheme: dark)'
@@ -32,11 +48,8 @@ if (isDarkMode) {
const container = document.getElementById('root');
-// Store the root outside the if block so it can be accessed by the HMR code
-let root: any;
-
if (container) {
- root = createRoot(container);
+ const root = createRoot(container);
root.render(
@@ -49,24 +62,3 @@ if (container) {
);
}
-
-// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
-// Learn more: https://www.webpackjs.com/concepts/hot-module-replacement/
-if (module.hot) {
- module.hot.accept('./App', () => {
- // New version of App component imported
- if (root) {
- root.render(
-
-
-
-
-
-
-
-
-
- );
- }
- });
-}
diff --git a/webpack.config.js b/webpack.config.js
index 710fe93..797266b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,8 +1,8 @@
const path = require('path');
-const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
+const webpack = require('webpack');
const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -63,6 +63,16 @@ module.exports = {
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
isDevelopment && new webpack.HotModuleReplacementPlugin(),
+ new webpack.DefinePlugin(
+ Object.fromEntries(
+ Object.entries({
+ ENABLE_NOTE_COLOR: process.env.ENABLE_NOTE_COLOR,
+ }).map(([key, value]) => [
+ `process.env.${key}`,
+ JSON.stringify(value),
+ ])
+ )
+ ),
new HtmlWebpackPlugin({
title: 'tududi',
filename: 'index.html',