Notes page revamp! (#487)
* Setup wide layout * fixup! Setup wide layout * Setup new layout
This commit is contained in:
parent
92f12aad77
commit
72d0baeb75
14 changed files with 1299 additions and 412 deletions
|
|
@ -4,5 +4,5 @@ module.exports = {
|
|||
'config': path.resolve('backend', 'config', 'database.js'),
|
||||
'models-path': path.resolve('backend', 'models'),
|
||||
'seeders-path': path.resolve('backend', 'seeders'),
|
||||
'migrations-path': path.resolve('migrations')
|
||||
'migrations-path': path.resolve('backend', 'migrations')
|
||||
};
|
||||
|
|
|
|||
24
backend/migrations/20251106000001-add-color-to-notes.js
Normal file
24
backend/migrations/20251106000001-add-color-to-notes.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
safeAddColumns,
|
||||
safeRemoveColumn,
|
||||
} = require('../utils/migration-utils');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await safeAddColumns(queryInterface, 'notes', [
|
||||
{
|
||||
name: 'color',
|
||||
definition: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await safeRemoveColumn(queryInterface, 'notes', 'color');
|
||||
},
|
||||
};
|
||||
|
|
@ -40,6 +40,10 @@ module.exports = (sequelize) => {
|
|||
key: 'id',
|
||||
},
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'notes',
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ router.get(
|
|||
// POST /api/note
|
||||
router.post('/note', async (req, res) => {
|
||||
try {
|
||||
const { title, content, project_uid, project_id, tags } = req.body;
|
||||
const { title, content, project_uid, project_id, tags, color } =
|
||||
req.body;
|
||||
|
||||
const noteAttributes = {
|
||||
title,
|
||||
|
|
@ -150,6 +151,11 @@ router.post('/note', async (req, res) => {
|
|||
user_id: req.session.userId,
|
||||
};
|
||||
|
||||
// Add color if provided
|
||||
if (color !== undefined) {
|
||||
noteAttributes.color = color;
|
||||
}
|
||||
|
||||
// Support both project_uid (new) and project_id (legacy)
|
||||
const projectIdentifier = project_uid || project_id;
|
||||
|
||||
|
|
@ -262,11 +268,13 @@ router.patch(
|
|||
where: { uid: req.params.uid },
|
||||
});
|
||||
|
||||
const { title, content, project_uid, project_id, tags } = req.body;
|
||||
const { title, content, project_uid, project_id, tags, color } =
|
||||
req.body;
|
||||
|
||||
const updateData = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (content !== undefined) updateData.content = content;
|
||||
if (color !== undefined) updateData.color = color;
|
||||
|
||||
// Handle project assignment - support both project_uid (new) and project_id (legacy)
|
||||
const projectIdentifier =
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
login,
|
||||
navigateAndWait,
|
||||
clickAndWaitForModal,
|
||||
waitForElement,
|
||||
createUniqueEntity,
|
||||
waitForNetworkIdle
|
||||
} from '../helpers/testHelpers';
|
||||
|
||||
// Navigate to notes page after login
|
||||
async function loginAndNavigateToNotes(page, baseURL) {
|
||||
const appUrl = await login(page, baseURL);
|
||||
await navigateAndWait(page, appUrl + '/notes');
|
||||
await expect(page).toHaveURL(/\/notes/);
|
||||
return appUrl;
|
||||
}
|
||||
|
||||
// Create a note with checkbox content
|
||||
async function createNoteWithCheckboxes(page, noteTitle) {
|
||||
const addNoteButton = page.locator('[data-testid="add-note-button"]');
|
||||
const titleInput = page.locator('[data-testid="note-title-input"]');
|
||||
|
||||
await clickAndWaitForModal(addNoteButton, titleInput);
|
||||
|
||||
// Fill in the note title
|
||||
await titleInput.click();
|
||||
await titleInput.clear();
|
||||
await titleInput.type(noteTitle, { delay: 50 });
|
||||
|
||||
// Fill in note content with checkboxes
|
||||
const contentTextarea = page.locator('[data-testid="note-content-textarea"]');
|
||||
await contentTextarea.click();
|
||||
await contentTextarea.fill('# Shopping List\n\n- [ ] Buy milk\n- [ ] Buy eggs\n- [ ] Buy bread');
|
||||
|
||||
// Save the note
|
||||
await page.locator('[data-testid="note-save-button"]').click();
|
||||
|
||||
// Wait for the modal to close
|
||||
await waitForElement(titleInput, { state: 'hidden' });
|
||||
await waitForNetworkIdle(page);
|
||||
}
|
||||
|
||||
test('user can toggle checkboxes in note detail view and changes are saved', async ({ page, baseURL }) => {
|
||||
await loginAndNavigateToNotes(page, baseURL);
|
||||
|
||||
// Create a note with checkboxes
|
||||
const noteTitle = createUniqueEntity('Shopping List');
|
||||
await createNoteWithCheckboxes(page, noteTitle);
|
||||
|
||||
// Click on the note to view it
|
||||
const noteLink = page.locator('a').filter({ hasText: noteTitle });
|
||||
await expect(noteLink).toBeVisible();
|
||||
await noteLink.click();
|
||||
|
||||
// Wait for note detail page to load
|
||||
await expect(page).toHaveURL(/\/note\//);
|
||||
|
||||
// Wait for checkboxes to be visible
|
||||
const checkboxes = page.locator('input[type="checkbox"]');
|
||||
await expect(checkboxes.first()).toBeVisible();
|
||||
|
||||
// Verify we have 3 checkboxes
|
||||
await expect(checkboxes).toHaveCount(3);
|
||||
|
||||
// Verify all checkboxes are initially unchecked
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await expect(checkboxes.nth(i)).not.toBeChecked();
|
||||
}
|
||||
|
||||
// Click the first checkbox (Buy milk)
|
||||
await checkboxes.first().click();
|
||||
|
||||
// Wait for the save to complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify the checkbox is now checked
|
||||
await expect(checkboxes.first()).toBeChecked();
|
||||
|
||||
// Refresh the page to verify the change was saved
|
||||
await page.reload();
|
||||
await expect(checkboxes.first()).toBeVisible();
|
||||
|
||||
// Verify the first checkbox is still checked after reload
|
||||
await expect(checkboxes.first()).toBeChecked();
|
||||
|
||||
// Verify other checkboxes are still unchecked
|
||||
await expect(checkboxes.nth(1)).not.toBeChecked();
|
||||
await expect(checkboxes.nth(2)).not.toBeChecked();
|
||||
|
||||
// Toggle the first checkbox back to unchecked
|
||||
await checkboxes.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify it's now unchecked
|
||||
await expect(checkboxes.first()).not.toBeChecked();
|
||||
});
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
login,
|
||||
navigateAndWait,
|
||||
clickAndWaitForModal,
|
||||
fillInputReliably,
|
||||
waitForElement,
|
||||
hoverAndWaitForVisible,
|
||||
confirmDialog,
|
||||
createUniqueEntity,
|
||||
waitForNetworkIdle
|
||||
} from '../helpers/testHelpers';
|
||||
|
||||
// Navigate to notes page after login
|
||||
async function loginAndNavigateToNotes(page, baseURL) {
|
||||
const appUrl = await login(page, baseURL);
|
||||
|
||||
// Navigate to notes page
|
||||
await navigateAndWait(page, appUrl + '/notes');
|
||||
await expect(page).toHaveURL(/\/notes/);
|
||||
|
||||
return appUrl;
|
||||
}
|
||||
|
||||
// Shared function to create a note via the sidebar button
|
||||
async function createNote(page, noteTitle, noteContent = '') {
|
||||
// Find and click the "Add Note" button in the sidebar
|
||||
const addNoteButton = page.locator('[data-testid="add-note-button"]');
|
||||
const titleInput = page.locator('[data-testid="note-title-input"]');
|
||||
|
||||
await clickAndWaitForModal(addNoteButton, titleInput);
|
||||
|
||||
// Fill in the note title
|
||||
await titleInput.click();
|
||||
await titleInput.clear();
|
||||
await titleInput.type(noteTitle, { delay: 50 });
|
||||
|
||||
// Fill in the note content if provided
|
||||
if (noteContent) {
|
||||
const contentTextarea = page.locator('[data-testid="note-content-textarea"]');
|
||||
await contentTextarea.click();
|
||||
await contentTextarea.fill(noteContent);
|
||||
}
|
||||
|
||||
// Save the note using the specific test ID
|
||||
await page.locator('[data-testid="note-save-button"]').click();
|
||||
|
||||
// Wait for the modal to close
|
||||
await waitForElement(titleInput, { state: 'hidden' });
|
||||
|
||||
// Wait for note creation to complete
|
||||
await waitForNetworkIdle(page);
|
||||
}
|
||||
|
||||
test('user can create a new note and verify it appears in the notes list', async ({ page, baseURL }) => {
|
||||
await loginAndNavigateToNotes(page, baseURL);
|
||||
|
||||
// Create a unique test note
|
||||
const noteTitle = createUniqueEntity('Test Note');
|
||||
const noteContent = 'This is test content for note';
|
||||
await createNote(page, noteTitle, noteContent);
|
||||
|
||||
// Verify the note appears in the notes list
|
||||
await expect(page.getByText(noteTitle)).toBeVisible();
|
||||
});
|
||||
|
||||
test('user can update an existing note', async ({ page, baseURL }) => {
|
||||
await loginAndNavigateToNotes(page, baseURL);
|
||||
|
||||
// Create an initial note
|
||||
const originalNoteTitle = createUniqueEntity('Test note to edit');
|
||||
const originalNoteContent = 'Original content';
|
||||
await createNote(page, originalNoteTitle, originalNoteContent);
|
||||
|
||||
// Find the specific note card by title text
|
||||
const noteCard = page.locator('a').filter({ hasText: originalNoteTitle });
|
||||
await expect(noteCard).toBeVisible();
|
||||
|
||||
// Hover over the note card and wait for dropdown button to be visible
|
||||
const dropdownButton = noteCard.locator('..').locator('button[data-testid^="note-dropdown-"]');
|
||||
await hoverAndWaitForVisible(noteCard, dropdownButton);
|
||||
|
||||
// Click dropdown button
|
||||
await dropdownButton.click();
|
||||
|
||||
// Wait for dropdown menu to appear and click Edit
|
||||
const editButton = page.locator('button[data-testid^="note-edit-"]').first();
|
||||
await waitForElement(editButton);
|
||||
await editButton.click();
|
||||
|
||||
// Wait for the Note Modal to appear with the note data
|
||||
const noteTitleInput = page.locator('[data-testid="note-title-input"]');
|
||||
await waitForElement(noteTitleInput);
|
||||
|
||||
// Verify the note title field is pre-filled
|
||||
await expect(noteTitleInput).toHaveValue(originalNoteTitle);
|
||||
|
||||
// Edit the note title and content
|
||||
const editedNoteTitle = createUniqueEntity('Edited test note');
|
||||
const editedNoteContent = 'Edited content';
|
||||
await fillInputReliably(noteTitleInput, editedNoteTitle);
|
||||
|
||||
const noteContentTextarea = page.locator('[data-testid="note-content-textarea"]');
|
||||
await noteContentTextarea.clear();
|
||||
await noteContentTextarea.fill(editedNoteContent);
|
||||
|
||||
// Save the changes
|
||||
await page.locator('[data-testid="note-save-button"]').click();
|
||||
|
||||
// Wait for the modal to close
|
||||
await waitForElement(noteTitleInput, { state: 'hidden' });
|
||||
|
||||
// Verify the edited note appears in the notes list
|
||||
await expect(page.getByText(editedNoteTitle)).toBeVisible();
|
||||
|
||||
// Verify the original note title is no longer visible
|
||||
await expect(page.getByText(originalNoteTitle)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('user can delete an existing note', async ({ page, baseURL }) => {
|
||||
await loginAndNavigateToNotes(page, baseURL);
|
||||
|
||||
// Create an initial note
|
||||
const noteTitle = createUniqueEntity('Test note to delete');
|
||||
const noteContent = 'Content to delete';
|
||||
await createNote(page, noteTitle, noteContent);
|
||||
|
||||
// Find the specific note card by title text
|
||||
const noteCard = page.locator('a').filter({ hasText: noteTitle });
|
||||
await expect(noteCard).toBeVisible();
|
||||
|
||||
// Hover over the note card and wait for dropdown button to be visible
|
||||
const dropdownButton = noteCard.locator('..').locator('button[data-testid^="note-dropdown-"]');
|
||||
await hoverAndWaitForVisible(noteCard, dropdownButton);
|
||||
|
||||
// Click dropdown button
|
||||
await dropdownButton.click();
|
||||
|
||||
// Wait for dropdown menu to appear and click Delete
|
||||
const deleteButton = page.locator('button[data-testid^="note-delete-"]').first();
|
||||
await waitForElement(deleteButton);
|
||||
await deleteButton.click();
|
||||
|
||||
// Wait for and handle the confirmation dialog
|
||||
await confirmDialog(page, 'Delete Note');
|
||||
|
||||
// Verify the note is no longer visible in the notes list
|
||||
await expect(page.getByRole('link', { name: new RegExp(noteTitle) })).not.toBeVisible();
|
||||
});
|
||||
|
|
@ -238,6 +238,7 @@ const App: React.FC = () => {
|
|||
element={<ViewDetail />}
|
||||
/>
|
||||
<Route path="/notes" element={<Notes />} />
|
||||
<Route path="/notes/:uid" element={<Notes />} />
|
||||
<Route
|
||||
path="/note/:uidSlug"
|
||||
element={<NoteDetails />}
|
||||
|
|
|
|||
|
|
@ -459,9 +459,9 @@ const Layout: React.FC<LayoutProps> = ({
|
|||
>
|
||||
<div className="flex flex-col bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 min-h-screen overflow-y-auto">
|
||||
<div
|
||||
className={`flex-grow py-6 px-2 md:px-6 transition-all duration-300 ${
|
||||
isMobileSearchOpen ? 'pt-32' : 'pt-24'
|
||||
} md:pt-24`}
|
||||
className={`flex-grow py-0 px-0 md:px-4 transition-all duration-300 ${
|
||||
isMobileSearchOpen ? 'pt-32' : 'pt-20'
|
||||
} md:pt-20`}
|
||||
>
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@ interface MarkdownRendererProps {
|
|||
className?: string;
|
||||
summaryMode?: boolean;
|
||||
onContentChange?: (newContent: string) => void;
|
||||
noteColor?: string;
|
||||
}
|
||||
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
|
|
@ -16,7 +17,22 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
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<MarkdownRendererProps> = ({
|
|||
h1: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h1
|
||||
className="text-3xl font-bold mb-4 text-gray-900 dark:text-gray-100"
|
||||
className={`text-3xl font-bold mb-4 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h2
|
||||
className="text-2xl font-semibold mb-3 text-gray-900 dark:text-gray-100"
|
||||
className={`text-2xl font-semibold mb-3 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h3
|
||||
className="text-xl font-medium mb-2 text-gray-900 dark:text-gray-100"
|
||||
className={`text-xl font-medium mb-2 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h4: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h4
|
||||
className="text-lg font-medium mb-2 text-gray-900 dark:text-gray-100"
|
||||
className={`text-lg font-medium mb-2 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h5: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h5
|
||||
className="text-base font-medium mb-2 text-gray-900 dark:text-gray-100"
|
||||
className={`text-base font-medium mb-2 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h6: ({ ...props }) =>
|
||||
summaryMode ? (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<h6
|
||||
className="text-sm font-medium mb-2 text-gray-900 dark:text-gray-100"
|
||||
className={`text-sm font-medium mb-2 ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
@ -165,7 +217,10 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
// Customize paragraph styles
|
||||
p: ({ ...props }) => (
|
||||
<p
|
||||
className="mb-3 text-gray-700 dark:text-gray-300 leading-relaxed"
|
||||
className={`mb-3 leading-relaxed ${!noteColor ? 'text-gray-700 dark:text-gray-300' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
@ -173,17 +228,35 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
// Customize list styles
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="mb-3 list-disc list-inside space-y-1 text-gray-700 dark:text-gray-300"
|
||||
className={`mb-3 list-disc ml-4 pl-5 space-y-1 ${!noteColor ? 'text-gray-700 dark:text-gray-300' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="mb-3 list-decimal list-inside space-y-1 text-gray-700 dark:text-gray-300"
|
||||
className={`mb-3 list-decimal ml-4 pl-5 space-y-1 ${!noteColor ? 'text-gray-700 dark:text-gray-300' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => (
|
||||
<li
|
||||
className={
|
||||
!noteColor
|
||||
? 'text-gray-700 dark:text-gray-300'
|
||||
: ''
|
||||
}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => <li className="ml-4" {...props} />,
|
||||
|
||||
// Customize link styles
|
||||
a: ({ ...props }) => (
|
||||
|
|
@ -225,7 +298,7 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
}
|
||||
return (
|
||||
<code
|
||||
className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100"
|
||||
className="py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -293,7 +366,10 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
// Customize strong/bold text
|
||||
strong: ({ ...props }) => (
|
||||
<strong
|
||||
className="font-semibold text-gray-900 dark:text-gray-100"
|
||||
className={`font-semibold ${!noteColor ? 'text-gray-900 dark:text-gray-100' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
@ -301,7 +377,10 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
// Customize italic text
|
||||
em: ({ ...props }) => (
|
||||
<em
|
||||
className="italic text-gray-700 dark:text-gray-300"
|
||||
className={`italic ${!noteColor ? 'text-gray-700 dark:text-gray-300' : ''}`}
|
||||
style={{
|
||||
color: textColor,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
21
frontend/config/featureFlags.ts
Normal file
21
frontend/config/featureFlags.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<BrowserRouter>
|
||||
|
|
@ -49,24 +62,3 @@ if (container) {
|
|||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<BrowserRouter>
|
||||
<ToastProvider>
|
||||
<TelegramStatusProvider>
|
||||
<App />
|
||||
</TelegramStatusProvider>
|
||||
</ToastProvider>
|
||||
</BrowserRouter>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue